Skip to content

[Code Health] DFX collectors are worker-level objects with run-level thread lifetime #2078

Description

@ChaoWao

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:180DepGenCollector
  • src/common/platform/onboard/host/device_runner_base.cpp:1852start_shared_collectors_for_run()
  • src/common/platform/onboard/host/device_runner_base.cpp:1887teardown_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-748diagnostics_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-130wait_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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    code healthTechnical debt, robustness, code quality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions