Skip to content

[Feature] Host swimlane (L3/L4) — the missing third DFX timeline #1708

Description

@ChaoWao

Summary

Add a host swimlane: a Perfetto/Chrome-trace timeline of one run's host-side
execution (graph build → orchestrator submit → scheduler dispatch → waiting on chip
children → completion), emitted from the [STRACE] markers we already produce.

simpler has two of the three levels covered (see #995):

Level Answers Status
core swimlane L0 which pipe (MTE2/CUBE/FIXP/…) a task stalls on inside the core ✅ ships
chip swimlane L2 when each task ran on device, and where the AICPU scheduler loop spent its time ✅ ships
host swimlane L3/L4 why a task started when it did ❌ missing

The three are successive zoom levels, not alternatives. L2 says "this task ran from t1 to
t2"; L0 says "here is why it was slow inside the core"; nothing today says why it only
started at t1.

Motivation / Use Case

When a run is slower than expected and the device timeline looks fine, there is currently
no timeline to look at. Concretely unanswerable today:

Two extra reasons this is worth doing as its own feature:

  1. The data already exists. [STRACE] markers already carry real pid, real tid,
    invocation id, name, ts, dur, clock, and open-ended k=v attributes; host
    timestamps are CLOCK_MONOTONIC and comparable across the parent and its forked
    children
    . Unknown k=v fields are ignored by the parser, so adding attributes needs
    no marker-version bump.
  2. The current viewer discards it. strace_timing.py --trace-out reads the real tid
    and then throws it away, synthesizing a dense lane id from (pid, invocation).
    Consequences: one lane per invocation rather than per thread, so a single L4 run
    fans out to (1 + N_L3 + N_L2) × N_inv flat lanes, and spans from one thread get
    split across lanes by invocation
    — which is exactly what makes "is Simplify AicpuExecutor API and unify naming conventions #7 slower than support extern func define in aicore #6"
    impossible to see.

Proposed API / Behavior

New output mode; --trace-out stays byte-identical (it is a per-invocation call-tree view,
a legitimate and different question):

python -m simpler_setup.tools.strace_timing <log> --swimlane host_swimlane.json
# Chrome Trace JSON -> Perfetto, same viewer as chip swimlane

Lanes are the real OS pid / tid — no synthetic numbering. The roles already map
1:1 onto real threads, so real ids are sufficient and strictly more useful:

  • Scheduler owns a dedicated sched_thread_
  • WorkerThread is documented as "one worker, one std::thread"
  • Orchestrator has no thread of its own — it runs on the caller (Python facade) thread
  • each chip child is a real fork(), so it already has its own pid

Real ids additionally let a busy lane be taken straight to perf top -t <tid>,
gdb -p <pid>, or /proc, and need no mapping table that can drift. The only thing to add
is process_name / thread_name metadata so Perfetto shows names instead of bare numbers —
a labelling concern, not an identity one.

Spans — one per real decision point (deliberately not the internal lane CAS or every
poll_progress iteration; those are too hot to instrument):

span site
l3.graph_build Python Worker._submit_l3_locked, around the serialized graph callback
l3.submit Orchestrator::submit_next_level, after task-slot allocation
l3.dispatch WorkerThread::enqueue_dispatch, after successful publication
l3.frame_submit LocalMailboxEndpoint::submit_progress
l3.activate activate_progress
l3.complete finish_progress_dispatch

Scheduler phases (optional second tier — the loop is hot enough that instrumenting
every iteration would perturb what it measures). Scheduler::run() has the same shape as
the AICPU loop that chip swimlane already profiles:

loop {
    completion_cv_.wait(...)     // sched.wait
    Phase 1: drain completions   // sched.drain
    Phase 2: dispatch_ready()    // sched.dispatch
}

That split is the direct answer to "is the host starved, or slow to dispatch?".

Three cases the design has to serve:

  • L3/L2 run interleaving — falls out of real ids for free: parent and child are
    different pids on a shared CLOCK_MONOTONIC, so overlapping runs are visibly
    overlapping. Requires only that the child actually emits spans.
  • Detail inside one worker.run — spans on the same (pid, tid) nest by ts/dur
    containment automatically. But l3.graph_build (facade thread) and l3.dispatch
    (WorkerThread) are on different tids and will not nest
    — that pair needs an explicit
    Chrome flow event (ph:"s"/ph:"f"). This is the real reason flow arrows are needed,
    not aesthetics.
  • Device timing info printed by the host — already emitted: the host prints device-domain
    phases after readback as clk=dev spans under simpler_run.runner_run.device_wall
    (strace_timing.py derives its Device/Orch/Sched/Effective columns from exactly these).
    Draw them on a separate device sub-track, keeping the clk=dev tag and doing no
    alignment
    . Rendering them alongside is not the same as placing them on the host axis —
    the latter needs a measured clock anchor and is explicitly out of scope. Merging them into
    the host axis would look aligned while being wrong, which is worse than keeping them apart.

Gating: reuse the existing SIMPLER_HOST_STRACE compile-time gate. No new environment
variable or macro.
When off, C++ markers and the logger lookup compile out, Python takes
its direct-call path, and throughput must not regress.

Alternatives Considered

  • Extend --trace-out instead of adding a mode — rejected: its per-invocation grouping
    is the right shape for a call-tree view and may have consumers; the two answer different
    questions and should coexist.
  • Synthesize role-based lane ids (tid 0=facade, 1=orch, 2=sched, 10+=chip) — rejected
    after checking the code: the roles already correspond 1:1 to real threads, so synthetic ids
    would re-number real ones with zero added information while losing perf/gdb//proc
    correlation and adding a mapping table that can drift.
  • Go straight to a unified L4→L3→L2→L0 trace (shared pid numbering, host/device clock
    alignment, cross-level flow arrows) — deferred. The dominant use case is looking at one
    level at a time, and bundling the independent, immediately useful piece (host has no
    timeline at all) with the two hard ones (clock calibration, hierarchy folding) would block
    the former on the latter.

Additional Context

Sub-issue of #995 (DFX capability tracking), alongside the two shipping swimlanes:

  • docs/dfx/chip-swimlane-profiling.md — L2
  • docs/dfx/core-swimlane-profiling.md — L0

Two forward-compatibility constraints, so a future unified view does not require redoing
this work — neither adds effort now:

  1. Carry the run/task/endpoint keys as span attributes (they are needed for joins anyway).
  2. Keep device timestamps tagged clk=dev with no fake alignment — applying a guessed
    offset now would make every historical trace untrustworthy later.

Landing constraints worth stating up front:

  • One sink. All spans cross the _task_interface extension boundary; add a non-variadic
    host-span emit C entry point to the existing process-global libsimpler_log.so. Do not
    compile a private logger into _task_interface and do not introduce a second Python
    grammar.
  • Preload before fork(). The child must inherit an already-loaded logger. Getting the
    order wrong shows up as "the child's spans are all missing" rather than as an error.
  • Unsupported topologies disable quietly. A SUB-only L3, an L4+ add_worker, or a
    remote-only parent has no _l3_bins logger path; the bridge stays off and initialization
    must still succeed.

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions