Scheduler-Overhead Model¶
The timeline model behind sched_overhead_analysis (text report) and the
swimlane_converter --overhead track (8 Perfetto counter lines). Both answer
one question: when is makespan time wasted because a free core has ready work
the scheduler hasn't placed — vs. legitimately busy or dependency-limited?
Both tools need two artifacts, captured in separate runs (co-running them perturbs the swimlane timing):
chip_swimlane_records.json— per-task timing, from--enable-chip-swimlane(level ≥ 3 for the scheduler-loop parts).deps.json— the task DAG, from a separate--enable-dep-genrun.
Timeline basis¶
- Window (makespan) =
[first aicore start, last aicore end]=[min(start), max(end)]. AICore start/end timestamps are authoritative. - Per task:
dispatch(AICPU wrote the descriptor) →start(core began the kernel) →end(kernel done) →finish(AICPU observed completion). - All shares are % of the makespan (dependency-aware), never core-time — core-time treats an idle engine as waste even when its work is genuinely done.
Readiness (ready)¶
ready(T) = max over T's producers of producer end_time — the data exists once
the producer kernel ends. Two refinements that matter:
- Off-perf predecessors. A task whose predecessors are all absent from the
perf set (e.g. host/DMA input loads) falls back to its own dispatch, NOT
the window start. Defaulting to
w0would mark it "ready from t=0" and invent ~2% of false early overhead during the dispatch ramp. - MIX tasks. A task with records on both engines (one
task_id, AIC + AIV) counts as ready work for both — it needs both engines to launch. Attributing it to one engine (its first record) hides the other engine's wait.
Overhead (the core metric)¶
Per instant, for each core type T (AIC / AIV) independently:
overhead(T) ⇔ idle T-core exists (k_T − running_T > 0)
AND a ready, UNDISPATCHED T-task exists ([ready, dispatch])
- A dispatched-but-not-started task is not counted — the scheduler already
placed it; its
[dispatch, start]pickup is aicore switch (below), not overhead. - An engine with no ready work (
ready == 0) is not overhead — its idle cores are dependency-mandated (e.g. AIV idle through an AIC-heavy tail). That cost shows as low parallelism, not as wasted scheduler time.
System aggregates over the present engines:
| Line | Definition |
|---|---|
all_overhead |
every present engine is overhead (whole chip blocked — e.g. a MIX waiting to launch) |
has_overhead |
every engine that has ready work is overhead (engines with no work ignored) |
all_overhead ≤ per-engine ≤ has_overhead. On qwen3-14b decode_layer
(a2a3, 542 tasks): AIC 15.0%, AIV 10.3%, all 3.3%, has 20.2%.
aicore switch¶
On a core, the gap [prev_end, start] of a task whose dispatch < prev_end
(pre-dispatched / pending pickup). The core picks up an already-issued task; the
gap is the pickup latency, ~0.8 µs each.
- Report it per core (~8–11 µs/core), never the all-cores sum (~240 µs reads as a scary aggregate but switches on different cores overlap).
- A switch is overhead when the engine has other ready work at that instant (the idle pickup core coincides with undispatched-ready work), else independent. On the sample data ~63% of AIC switch falls in overhead.
- Makespan switch bound:
lower = min over all cores,upper = sum of per-engine minima(best core per phase on the critical path). Sample:[0, 8.10 µs]=[0%, 0.8%]of makespan — the switch the makespan truly pays is tiny; most switch time overlaps overhead anyway.
Tooling¶
swimlane_converter --overhead¶
Adds 8 counter tracks under the AICPU Scheduler process (pid=3,
oh_-prefixed) so they overlay the AICore task bars:
oh_{aic,aiv}_idle core count not executing (k − running)
oh_{aic,aiv}_ready undispatched-ready task count (MIX counts for both)
oh_{aic,aiv}_overhead 0/1 = idle>0 AND ready>0
oh_all_overhead 0/1 = every engine overhead
oh_has_overhead 0/1 = every working engine overhead
python -m simpler_setup.tools.swimlane_converter <perf>.json \
--deps-json <deps>.json --overhead -o out.json # drag into ui.perfetto.dev
sched_overhead_analysis¶
| Part | Content |
|---|---|
| 1 | Overhead verdict — per-engine + system all/has overhead (% of makespan) |
| 2 | aicore switch — per-core min/mean/max, overhead-vs-independent split, makespan bound |
| 3 / 4 | Head OH / Tail OH distributions |
| 5 | AICPU scheduler-loop budget — separate S/P ns/loop, all mutually exclusive outer phases, standalone HBG P-thread Resolve, pop hit-rate, fanout/fanin |
| 6 | Critical-path attribution — compute vs scheduler-injected µs on the makespan path |
python -m simpler_setup.tools.sched_overhead_analysis \
--chip-swimlane-records-json <perf>.json --deps-json <deps>.json
For TMR captures, Resolve is nested in Complete or Dummy and is excluded from
the phase total to avoid double counting. For HBG captures, Resolve is
standalone work on the P thread and is included. The two are told apart by the
resolve_standalone phase discriminator the HBG P thread emits, not by
timestamp containment; a capture predating that discriminator falls back to
containment, where a Resolve ending exactly at its Complete or Dummy parent's
end counts as standalone. Both spellings report under the resolve label.
Empty HBG async polling is reported as compact AsyncPoll(0) bars, so its
measured CPU cost contributes to the scheduler budget instead of being
reconstructed as idle. HBG's S
threads detect AICore FIN and dispatch work, while its P thread resolves
completion state and dependencies. Part 5 reports their loop rates separately;
the Tail-OH-to-loop comparison uses only S-thread loops because Tail OH ends at
FIN detection, before P-thread resolution begins. Phase totals still sum all
threads and therefore include P-thread CPU cost. The phase table lists only
phases represented by the capture, so runtime-specific absent phases do not
appear as synthetic zero rows.