You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Did the time go to graph build, to scheduler dispatch, or to waiting on a child?
Is the host scheduler the bottleneck, and if so is it starved for work or slow to dispatch?
Did run N+1's host-side preparation actually overlap run N's device execution?
Two extra reasons this is worth doing as its own feature:
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.
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:
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:
Carry the run/task/endpoint keys as span attributes (they are needed for joins anyway).
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.
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.simplerhas two of the three levels covered (see #995):MTE2/CUBE/FIXP/…) a task stalls on inside the coreThe 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:
[STRACE]markers already carry realpid, realtid,invocation id, name,
ts,dur, clock, and open-endedk=vattributes; hosttimestamps are
CLOCK_MONOTONICand comparable across the parent and its forkedchildren. Unknown
k=vfields are ignored by the parser, so adding attributes needsno marker-version bump.
strace_timing.py --trace-outreads the realtidand 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_invflat lanes, and spans from one thread getsplit 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-outstays byte-identical (it is a per-invocation call-tree view,a legitimate and different question):
Lanes are the real OS
pid/tid— no synthetic numbering. The roles already map1:1 onto real threads, so real ids are sufficient and strictly more useful:
Schedulerowns a dedicatedsched_thread_WorkerThreadis documented as "one worker, one std::thread"Orchestratorhas no thread of its own — it runs on the caller (Python facade) threadfork(), so it already has its own pidReal 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 addis
process_name/thread_namemetadata 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_progressiteration; those are too hot to instrument):l3.graph_buildWorker._submit_l3_locked, around the serialized graph callbackl3.submitOrchestrator::submit_next_level, after task-slot allocationl3.dispatchWorkerThread::enqueue_dispatch, after successful publicationl3.frame_submitLocalMailboxEndpoint::submit_progressl3.activateactivate_progressl3.completefinish_progress_dispatchScheduler phases (optional second tier — the loop is hot enough that instrumenting
every iteration would perturb what it measures).
Scheduler::run()has the same shape asthe AICPU loop that chip swimlane already profiles:
That split is the direct answer to "is the host starved, or slow to dispatch?".
Three cases the design has to serve:
different pids on a shared
CLOCK_MONOTONIC, so overlapping runs are visiblyoverlapping. Requires only that the child actually emits spans.
worker.run— spans on the same(pid, tid)nest byts/durcontainment automatically. But
l3.graph_build(facade thread) andl3.dispatch(
WorkerThread) are on different tids and will not nest — that pair needs an explicitChrome flow event (
ph:"s"/ph:"f"). This is the real reason flow arrows are needed,not aesthetics.
phases after readback as
clk=devspans undersimpler_run.runner_run.device_wall(
strace_timing.pyderives its Device/Orch/Sched/Effective columns from exactly these).Draw them on a separate device sub-track, keeping the
clk=devtag and doing noalignment. 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_STRACEcompile-time gate. No new environmentvariable 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
--trace-outinstead of adding a mode — rejected: its per-invocation groupingis the right shape for a call-tree view and may have consumers; the two answer different
questions and should coexist.
tid 0=facade, 1=orch, 2=sched, 10+=chip) — rejectedafter 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//proccorrelation and adding a mapping table that can drift.
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— L2docs/dfx/core-swimlane-profiling.md— L0Two forward-compatibility constraints, so a future unified view does not require redoing
this work — neither adds effort now:
clk=devwith no fake alignment — applying a guessedoffset now would make every historical trace untrustworthy later.
Landing constraints worth stating up front:
_task_interfaceextension boundary; add a non-variadichost-span emit C entry point to the existing process-global
libsimpler_log.so. Do notcompile a private logger into
_task_interfaceand do not introduce a second Pythongrammar.
fork(). The child must inherit an already-loaded logger. Getting theorder wrong shows up as "the child's spans are all missing" rather than as an error.
add_worker, or aremote-only parent has no
_l3_binslogger path; the bridge stays off and initializationmust still succeed.