Related: #995
Category
Technical Debt (cleanup, refactor)
Component
Host Runtime
Description
The five DFX collectors are already members of the worker's device context, but their
threads are started and joined per run. That mismatch is the root of three separate
costs.
The objects:
| Collector |
Owner |
ChipSwimlaneCollector / ArgsDumpCollector / PmuCollector / ScopeStatsCollector |
DeviceRunnerBase |
DepGenCollector |
per-arch DeviceRunner |
The lifetime: start_shared_collectors_for_run() runs inside launch_run's arming block;
teardown_shared_collectors_after_run() runs in reap_run, joining the threads and doing
the actual artifact export. apply_call_config() binds both the five enables and
output_prefix_ from that run's CallConfig.
1. Enabling any DFX channel disables pipelining.
const bool allow_prepared_successor =
concurrent_native_prepare_supported_impl() != 0 && !config->diagnostics_any();
A single per-run collector set cannot serve two overlapping runs, so diagnostics_any()
forces depth back to 1. This is a cost paid today, on every diagnostic run.
2. Thread create/join on every run. Four to five threads spawned in the arming block
and joined at drain, per run.
3. The device-side producer has a non-optional dependency on a host consumer, and its
acquire loop does not time out when that consumer is absent.
dfx_backpressure_device.h states the contract: "Block-on-contention is the only
behavior — no opt-out gate." The free-queue depths are small
(PLATFORM_DEP_GEN_SLOT_COUNT = 4, PLATFORM_DUMP_SLOT_COUNT = 4,
PLATFORM_SCOPE_STATS_SLOT_COUNT = 8), so a producer starves quickly without a consumer.
The acquire loop in profiler_device_engine.h:
do {
if (head != tail) { ...; return true; }
mark_fq_contended(header, &contended_signalled);
if (!pop_freeze_barrier(header, Module::kBackpressureWaitCycles)) break;
} while (true);
pop_freeze_barrier spins only while fq_freeze_active != 0, and only the host sets
that flag. With no host consumer the barrier returns immediately every iteration, so the
30 s PLATFORM_DFX_BACKPRESSURE_TIMEOUT_CYCLES never arms — the header's own comment says
it is armed "only then and is the sole give-up (host dead/hung mid-freeze)". The result is
an unbounded spin on the AICPU, reaped only by the OS op-execute timeout. args_dump takes
a different path (wait_for_release, whose predicate is already true via fq_contended) and
does time out at 30 s — same subsystem, two failure shapes.
This third one is latent on main: every path that arms the device side also starts the
host side. It becomes reachable as soon as device work can be re-submitted without
re-entering the host bracket that owns the collector threads.
Location
src/common/platform/onboard/host/device_runner_base.h:1244,1250-1252 — collectors are worker-level members
src/a2a3/platform/onboard/host/device_runner.h:285, src/a5/platform/onboard/host/device_runner.h:180 — DepGenCollector
src/common/platform/onboard/host/device_runner_base.cpp:1852 — start_shared_collectors_for_run()
src/common/platform/onboard/host/device_runner_base.cpp:1887 — teardown_shared_collectors_after_run(), which also performs export
src/a2a3/platform/onboard/host/device_runner.cpp:606,705 — the per-run start / teardown bracket
src/common/platform/onboard/host/c_api_shared.cpp:747-748 — diagnostics_any() disabling allow_prepared_successor
src/common/platform/include/common/dfx_backpressure_device.h — "no opt-out gate"
src/common/platform/include/aicpu/profiler_device_engine.h:88-130 — wait_for_free_queue_entry acquire loop
src/common/platform/include/host/profiler_base.h:1198-1240 — drain loop; its idle timeout only logs and keeps the consumer alive
src/a2a3/platform/include/common/platform_config.h — free-queue depths and PLATFORM_DFX_BACKPRESSURE_TIMEOUT_CYCLES
Observed on 3e08ede76478a6e672c8dda44587ed6b3e4e7e01.
Proposed Fix
Move the thread bracket and the configuration binding from the run to the worker; keep a
separate, explicit boundary for record attribution.
Service = worker lifetime. Threads, device-side headers and queues live from
simpler_init to finalize_device. A consumer is then always present, which removes the
liveness hazard for every caller shape, removes the per-run thread churn, and removes the
reason diagnostics_any() has to force depth 1.
Attribution = explicit session. Today the run boundary is the collector boundary —
that is how records reach a run's output_prefix_. With a resident service that identity is
gone, so it needs to be stated: a session_begin(config, output_prefix) /
session_end() pair, where session_end reuses the existing open-drain-release machinery
(ProfilerAlgorithms::update_backpressure_freeze) to quiesce the pipeline and then export.
Draining rather than tagging avoids adding an epoch field to the record wire format and
avoids the race where a record produced in one session is drained in the next. The
precondition — the caller has quiesced the device — is natural for a diagnostic boundary and
is already satisfied at the point reap_run calls teardown today.
Wrapping the existing prepare_run / reap_run points in a session should leave current
behavior byte-identical; that equivalence is the regression bar.
Two behavior changes to decide explicitly:
- Per-run config becomes per-session. Today
apply_call_config re-binds the five
enables and output_prefix_ on every run, so run 1 can have PMU on and run 2 off. That
capability has to move onto the session or be dropped.
finish_clock_correlation_session() is per-run today and needs the same treatment.
Related: #995
Category
Technical Debt (cleanup, refactor)
Component
Host Runtime
Description
The five DFX collectors are already members of the worker's device context, but their
threads are started and joined per run. That mismatch is the root of three separate
costs.
The objects:
ChipSwimlaneCollector/ArgsDumpCollector/PmuCollector/ScopeStatsCollectorDeviceRunnerBaseDepGenCollectorDeviceRunnerThe lifetime:
start_shared_collectors_for_run()runs insidelaunch_run's arming block;teardown_shared_collectors_after_run()runs inreap_run, joining the threads and doingthe actual artifact export.
apply_call_config()binds both the five enables andoutput_prefix_from that run'sCallConfig.1. Enabling any DFX channel disables pipelining.
A single per-run collector set cannot serve two overlapping runs, so
diagnostics_any()forces depth back to 1. This is a cost paid today, on every diagnostic run.
2. Thread create/join on every run. Four to five threads spawned in the arming block
and joined at drain, per run.
3. The device-side producer has a non-optional dependency on a host consumer, and its
acquire loop does not time out when that consumer is absent.
dfx_backpressure_device.hstates the contract: "Block-on-contention is the onlybehavior — no opt-out gate." The free-queue depths are small
(
PLATFORM_DEP_GEN_SLOT_COUNT = 4,PLATFORM_DUMP_SLOT_COUNT = 4,PLATFORM_SCOPE_STATS_SLOT_COUNT = 8), so a producer starves quickly without a consumer.The acquire loop in
profiler_device_engine.h:pop_freeze_barrierspins only whilefq_freeze_active != 0, and only the host setsthat flag. With no host consumer the barrier returns immediately every iteration, so the
30 s
PLATFORM_DFX_BACKPRESSURE_TIMEOUT_CYCLESnever arms — the header's own comment saysit is armed "only then and is the sole give-up (host dead/hung mid-freeze)". The result is
an unbounded spin on the AICPU, reaped only by the OS op-execute timeout.
args_dumptakesa different path (
wait_for_release, whose predicate is already true viafq_contended) anddoes time out at 30 s — same subsystem, two failure shapes.
This third one is latent on main: every path that arms the device side also starts the
host side. It becomes reachable as soon as device work can be re-submitted without
re-entering the host bracket that owns the collector threads.
Location
src/common/platform/onboard/host/device_runner_base.h:1244,1250-1252— collectors are worker-level memberssrc/a2a3/platform/onboard/host/device_runner.h:285,src/a5/platform/onboard/host/device_runner.h:180—DepGenCollectorsrc/common/platform/onboard/host/device_runner_base.cpp:1852—start_shared_collectors_for_run()src/common/platform/onboard/host/device_runner_base.cpp:1887—teardown_shared_collectors_after_run(), which also performs exportsrc/a2a3/platform/onboard/host/device_runner.cpp:606,705— the per-run start / teardown bracketsrc/common/platform/onboard/host/c_api_shared.cpp:747-748—diagnostics_any()disablingallow_prepared_successorsrc/common/platform/include/common/dfx_backpressure_device.h— "no opt-out gate"src/common/platform/include/aicpu/profiler_device_engine.h:88-130—wait_for_free_queue_entryacquire loopsrc/common/platform/include/host/profiler_base.h:1198-1240— drain loop; its idle timeout only logs and keeps the consumer alivesrc/a2a3/platform/include/common/platform_config.h— free-queue depths andPLATFORM_DFX_BACKPRESSURE_TIMEOUT_CYCLESObserved on
3e08ede76478a6e672c8dda44587ed6b3e4e7e01.Proposed Fix
Move the thread bracket and the configuration binding from the run to the worker; keep a
separate, explicit boundary for record attribution.
Service = worker lifetime. Threads, device-side headers and queues live from
simpler_inittofinalize_device. A consumer is then always present, which removes theliveness hazard for every caller shape, removes the per-run thread churn, and removes the
reason
diagnostics_any()has to force depth 1.Attribution = explicit session. Today the run boundary is the collector boundary —
that is how records reach a run's
output_prefix_. With a resident service that identity isgone, so it needs to be stated: a
session_begin(config, output_prefix)/session_end()pair, wheresession_endreuses the existing open-drain-release machinery(
ProfilerAlgorithms::update_backpressure_freeze) to quiesce the pipeline and then export.Draining rather than tagging avoids adding an epoch field to the record wire format and
avoids the race where a record produced in one session is drained in the next. The
precondition — the caller has quiesced the device — is natural for a diagnostic boundary and
is already satisfied at the point
reap_runcalls teardown today.Wrapping the existing
prepare_run/reap_runpoints in a session should leave currentbehavior byte-identical; that equivalence is the regression bar.
Two behavior changes to decide explicitly:
apply_call_configre-binds the fiveenables and
output_prefix_on every run, so run 1 can have PMU on and run 2 off. Thatcapability has to move onto the session or be dropped.
finish_clock_correlation_session()is per-run today and needs the same treatment.