Skip to content

Add: expose L3/L4 host scheduling swimlane - #1730

Merged
ChaoWao merged 2 commits into
hw-native-sys:mainfrom
TaoZQY:feat/issue-1708-host-swimlane
Aug 13, 2026
Merged

ChaoWao merged 2 commits into
hw-native-sys:mainfrom
TaoZQY:feat/issue-1708-host-swimlane

Conversation

@TaoZQY

@TaoZQY TaoZQY commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a fixed, non-variadic host-span C ABI to the process-global logger and resolve it before local chip children are forked. The bridge reuses SIMPLER_HOST_STRACE and remains disabled for unsupported topologies.
  • Emit l3.graph_build, l3.submit, l3.dispatch, l3.frame_submit, l3.activate, and l3.complete with run, task-slot, worker, dispatch, and endpoint attributes.
  • Add strace_timing.py --swimlane to render real OS pid/tid lanes, process/thread labels, and submit-to-dispatch flow arrows without changing the established --trace-out view.
  • Keep raw clk=dev timestamps in unalignedDeviceSpans instead of putting unrelated device and host clocks on one visible Chrome Trace axis, which avoids an empty multi-day Perfetto viewport without inventing a clock offset.
  • Document the markers and CLI, and add coverage for graph failures, real lane identities, flow correlation, legacy-output compatibility, single-frame completion, and prepared-frame activation.

Usage

python -m simpler_setup.tools.strace_timing path/to/log \
  --swimlane host_swimlane.json

Open host_swimlane.json in Perfetto or chrome://tracing.
image

Testing

  • pytest tests/ut/py/test_strace_timing.py tests/ut/py/test_worker/test_host_worker.py -q — 226 passed
  • test_scheduler with host tracing enabled — 63 passed
  • test_scheduler builds with SIMPLER_HOST_STRACE=0
  • pytest examples/workers/l3/child_memory -q --platform a2a3sim — 1 passed

Fixes #1708

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e452ec1-f081-45f8-a992-16ea98fafa67

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a host-span ABI and logger bridge, instruments hierarchical execution, preloads the logger before extension loading, publishes runtime binaries atomically, and adds filtered host swimlane JSON output with real PID/TID lanes and unaligned device spans.

Changes

Host tracing and swimlane output

Layer / File(s) Summary
Host-span ABI and sink bridge
src/common/log/..., python/bindings/..., tests/ut/cpp/...
Defines the span ABI, weak sink declaration, bounded encoding, logger emission, RAII helpers, and Python binding functions.
Runtime initialization and graph-build tracing
python/simpler/..., .claude/rules/project-layout.md, tests/ut/py/test_worker/test_host_worker.py
Preloads libsimpler_log.so globally, centralizes logger initialization, gates host spans on sink availability, and emits l3.graph_build.
Hierarchical scheduler span instrumentation
src/common/hierarchical/..., tests/ut/cpp/...
Adds l3.submit, l3.dispatch, l3.complete, l3.frame_submit, and l3.activate spans with execution metadata and tests.
Atomic runtime artifact publication
simpler_setup/runtime_compiler.py, tests/ut/py/test_runtime_builder.py
Stages runtime binaries, applies processing before atomic replacement, and removes temporary files on failure.
Filtered host swimlane generation
simpler_setup/tools/..., tests/ut/py/test_strace_timing.py, docs/dfx/host-trace.md, simpler_setup/tools/README.md
Adds legacy-span filtering, real PID/TID lanes, dispatch flow events, unaligned device-span output, and the --swimlane CLI option.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonWorker
  participant HierarchicalScheduler
  participant HostLogger
  participant StraceTiming
  PythonWorker->>HostLogger: emit l3.graph_build
  HierarchicalScheduler->>HostLogger: emit submit, dispatch, frame, activation, completion spans
  HostLogger-->>StraceTiming: write STRACE records
  StraceTiming-->>StraceTiming: filter spans and build PID/TID lanes
  StraceTiming-->>StraceTiming: write flow events and unalignedDeviceSpans
Loading

Possibly related PRs

Poem

A rabbit watches host spans run,
Across real lanes beneath the sun.
Submit, dispatch, complete in flight,
Device clocks keep their separate light.
Atomic binaries land just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: exposing the L3/L4 host scheduling swimlane.
Description check ✅ Passed The description directly explains the host tracing, swimlane output, compatibility behavior, documentation, and test coverage.
Linked Issues check ✅ Passed The changes implement the linked issue objectives, including host spans, real PID/TID lanes, flow arrows, device-span separation, gating, and preload behavior.
Out of Scope Changes check ✅ Passed The changes support the host swimlane feature and its required logger loading, atomic publication, documentation, and test coverage.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/simpler/worker.py`:
- Around line 6475-6482: Update Worker.init() so
_initialize_simpler_log(binaries) and _bind_host_span_sink() run before any
local forks for every hierarchical worker, including L4 parents without
device_ids. If the sink bind returns false, quietly disable host tracing instead
of raising; set self._host_trace_enabled only when the bind succeeds.

In `@simpler_setup/tools/strace_timing.py`:
- Line 414: Rename the loop variable in the span.attrs iteration to avoid
shadowing the imported field symbol and resolve Ruff F402, updating its
references within the loop accordingly.

In `@src/common/hierarchical/worker_manager.cpp`:
- Around line 466-475: Capture the dispatch trace metadata before the worker is
notified: in the dispatch flow around cv_.notify_one(), read and store the run
ID, callable hash, and attributes from ring_ while d.task_slot is still valid.
After unlocking, emit l3.dispatch using only those captured values, preserving
the existing timing and notification behavior.

In `@src/common/log/host_log.cpp`:
- Around line 223-235: Update simpler_log_emit_host_span to bound the encoded
span->name and span->attributes fields so each formatted STRACE record remains
within PIPE_BUF, including truncation before logging. Escape newline and
field-delimiter characters using the format expected by the trace parser, while
preserving the existing validation and metadata fields.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c8861214-fbc6-497d-9951-312cbf8715f6

📥 Commits

Reviewing files that changed from the base of the PR and between 764f456 and 49281b0.

📒 Files selected for processing (17)
  • docs/dfx/host-trace.md
  • python/bindings/CMakeLists.txt
  • python/bindings/task_interface.cpp
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • simpler_setup/tools/README.md
  • simpler_setup/tools/strace_timing.py
  • src/common/hierarchical/host_trace.cpp
  • src/common/hierarchical/host_trace.h
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/worker_manager.cpp
  • src/common/log/host_log.cpp
  • src/common/log/include/common/host_span.h
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/py/test_strace_timing.py
  • tests/ut/py/test_worker/test_host_worker.py

Comment thread python/simpler/worker.py Outdated
Comment thread simpler_setup/tools/strace_timing.py Outdated
Comment thread src/common/hierarchical/worker_manager.cpp Outdated
Comment thread src/common/log/host_log.cpp
@TaoZQY
TaoZQY force-pushed the feat/issue-1708-host-swimlane branch 2 times, most recently from ec8a590 to 966d864 Compare August 11, 2026 04:20
@TaoZQY

TaoZQY commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
simpler_setup/tools/strace_timing.py (1)

425-436: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Thread labels depend on which span is parsed first.

_host_thread_name uses a single representative span per (pid, tid), chosen by parse order. If a thread emits an l3.* span without a role attribute first, the fallback at Line 431 labels the lane worker even when the thread is the facade or the scheduler.

Consider selecting the representative from the first span that carries a role attribute, and falling back to parse order only when no span on that lane has a role.

♻️ Proposed change
     for pid, tid in host_threads:
-        representative = next(span for span in host_spans if span.pid == pid and span.tid == tid)
+        lane_spans = [span for span in host_spans if span.pid == pid and span.tid == tid]
+        representative = next(
+            (span for span in lane_spans if "role" in attrs_by_id[id(span)]),
+            lane_spans[0],
+        )

Also applies to: 477-487

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@simpler_setup/tools/strace_timing.py` around lines 425 - 436, Update the
representative-span selection for each (pid, tid) lane before calling
_host_thread_name: prefer the first span carrying a role attribute, while
retaining parse-order selection when no span has a role. Ensure facade and
scheduler roles take precedence over inferred l3.* worker labels regardless of
span parse order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@simpler_setup/runtime_compiler.py`:
- Around line 347-348: Add a narrow, documented S603 noqa to the subprocess.run
call in _strip, stating that strip_bin comes from the controlled toolchain
selection; no direct change is needed at simpler_setup/runtime_compiler.py lines
347-348 beyond this suppression. Add documented S603 and S607 noqa exceptions to
the fixed test compiler invocation in tests/ut/py/test_runtime_builder.py lines
631-635, noting its test-only toolchain dependency.

In `@src/common/log/include/common/host_span.h`:
- Around line 35-50: Update the test capture sink’s implementation file to
define SIMPLER_LOG_HOST_SPAN_IMPL before including common/host_span.h, so its
simpler_log_emit_host_span definition is emitted strong. Document that test
sinks must set this macro, consistent with the existing host_log.cpp
implementation contract.

---

Nitpick comments:
In `@simpler_setup/tools/strace_timing.py`:
- Around line 425-436: Update the representative-span selection for each (pid,
tid) lane before calling _host_thread_name: prefer the first span carrying a
role attribute, while retaining parse-order selection when no span has a role.
Ensure facade and scheduler roles take precedence over inferred l3.* worker
labels regardless of span parse order.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6311d561-68de-49da-837e-5fdba48f82d0

📥 Commits

Reviewing files that changed from the base of the PR and between 89f15eb and 966d864.

📒 Files selected for processing (23)
  • .claude/rules/project-layout.md
  • docs/dfx/host-trace.md
  • python/bindings/CMakeLists.txt
  • python/bindings/task_interface.cpp
  • python/simpler/__init__.py
  • python/simpler/_log.py
  • python/simpler/_log_preload.py
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • simpler_setup/runtime_compiler.py
  • simpler_setup/tools/README.md
  • simpler_setup/tools/strace_timing.py
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/worker_manager.cpp
  • src/common/log/host_log.cpp
  • src/common/log/include/common/host_span.h
  • src/common/log/include/common/host_span_scope.h
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/a5/test_host_log_off.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/py/test_runtime_builder.py
  • tests/ut/py/test_strace_timing.py
  • tests/ut/py/test_worker/test_host_worker.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/common/hierarchical/orchestrator.cpp
  • simpler_setup/tools/README.md
  • tests/ut/py/test_strace_timing.py
  • python/simpler/worker.py
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/py/test_worker/test_host_worker.py
  • src/common/hierarchical/worker_manager.cpp
  • docs/dfx/host-trace.md

Comment thread simpler_setup/runtime_compiler.py Outdated
Comment thread src/common/log/include/common/host_span.h Outdated
@TaoZQY
TaoZQY force-pushed the feat/issue-1708-host-swimlane branch from 966d864 to a8bec15 Compare August 11, 2026 05:54
@TaoZQY

TaoZQY commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
simpler_setup/tools/strace_timing.py (1)

462-487: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Group the host spans once instead of rescanning per lane.

Line 466 rescans host_spans for every pid. Line 478 rescans it for every thread. Both are O(pids × spans) and O(threads × spans). A long capture contains many spans and many worker threads, so the cost grows quadratically.

♻️ Proposed refactor to a single grouping pass
-    host_pids = sorted({span.pid for span in host_spans})
-    host_threads = sorted({(span.pid, span.tid) for span in host_spans})
+    spans_by_pid: dict = {}
+    first_by_thread: dict = {}
+    for span in host_spans:
+        spans_by_pid.setdefault(span.pid, []).append(span)
+        first_by_thread.setdefault((span.pid, span.tid), span)
+    host_pids = sorted(spans_by_pid)
+    host_threads = sorted(first_by_thread)
 
     for pid in host_pids:
-        process_spans = [span for span in host_spans if span.pid == pid]
+        process_spans = spans_by_pid[pid]
         role = "host" if any(span.name.startswith("l3.") for span in process_spans) else "chip child"
@@
     for pid, tid in host_threads:
-        representative = next(span for span in host_spans if span.pid == pid and span.tid == tid)
+        representative = first_by_thread[(pid, tid)]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@simpler_setup/tools/strace_timing.py` around lines 462 - 487, Refactor the
host metadata generation around host_pids and host_threads to build a single
grouping of host_spans by (pid, tid), reusing it for both process and thread
metadata. Derive each process’s spans and role from that grouped data, and
obtain each thread representative without repeated scans of host_spans; preserve
the existing event names and _host_thread_name behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@simpler_setup/tools/strace_timing.py`:
- Around line 503-520: Update the submit/dispatch correlation around the submits
and dispatches collections to retain all submit spans for each _flow_key rather
than overwriting repeated task-slot entries. For each dispatch, select the
latest matching submit whose start timestamp is at or before the dispatch
timestamp, and use that submit as the source while preserving the existing
unmatched-dispatch behavior.
- Around line 522-548: The task dispatch events in the dispatch-event
construction should use a numeric or valid hexadecimal flow id instead of the
descriptive string built as flow_id. Preserve that descriptive identifier by
adding it to the event args, and ensure both the start and finish events reuse
the same valid id.

In `@tests/ut/py/test_runtime_builder.py`:
- Around line 636-638: Move the noqa suppression for S607 from the
subprocess.run statement line to the diagnostic line containing the "gcc"
argument, or collapse the call so the suppression covers both diagnostics;
preserve the existing subprocess.run behavior and S603 suppression.

---

Nitpick comments:
In `@simpler_setup/tools/strace_timing.py`:
- Around line 462-487: Refactor the host metadata generation around host_pids
and host_threads to build a single grouping of host_spans by (pid, tid), reusing
it for both process and thread metadata. Derive each process’s spans and role
from that grouped data, and obtain each thread representative without repeated
scans of host_spans; preserve the existing event names and _host_thread_name
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dbf09d94-043f-4f91-be92-1016a6683cc2

📥 Commits

Reviewing files that changed from the base of the PR and between 60312e4 and a8bec15.

📒 Files selected for processing (23)
  • .claude/rules/project-layout.md
  • docs/dfx/host-trace.md
  • python/bindings/CMakeLists.txt
  • python/bindings/task_interface.cpp
  • python/simpler/__init__.py
  • python/simpler/_log.py
  • python/simpler/_log_preload.py
  • python/simpler/task_interface.py
  • python/simpler/worker.py
  • simpler_setup/runtime_compiler.py
  • simpler_setup/tools/README.md
  • simpler_setup/tools/strace_timing.py
  • src/common/hierarchical/orchestrator.cpp
  • src/common/hierarchical/worker_manager.cpp
  • src/common/log/host_log.cpp
  • src/common/log/include/common/host_span.h
  • src/common/log/include/common/host_span_scope.h
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/a5/test_host_log_off.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/py/test_runtime_builder.py
  • tests/ut/py/test_strace_timing.py
  • tests/ut/py/test_worker/test_host_worker.py
🚧 Files skipped from review as they are similar to previous changes (20)
  • tests/ut/cpp/CMakeLists.txt
  • python/simpler/init.py
  • python/bindings/CMakeLists.txt
  • python/simpler/worker.py
  • src/common/hierarchical/orchestrator.cpp
  • .claude/rules/project-layout.md
  • src/common/log/include/common/host_span.h
  • simpler_setup/tools/README.md
  • python/simpler/_log_preload.py
  • tests/ut/py/test_strace_timing.py
  • tests/ut/py/test_worker/test_host_worker.py
  • src/common/log/include/common/host_span_scope.h
  • src/common/hierarchical/worker_manager.cpp
  • tests/ut/cpp/a5/test_host_log_off.cpp
  • docs/dfx/host-trace.md
  • src/common/log/host_log.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • python/bindings/task_interface.cpp
  • python/simpler/task_interface.py
  • python/simpler/_log.py

Comment thread simpler_setup/tools/strace_timing.py Outdated
Comment thread simpler_setup/tools/strace_timing.py Outdated
Comment thread tests/ut/py/test_runtime_builder.py Outdated
@TaoZQY
TaoZQY force-pushed the feat/issue-1708-host-swimlane branch 3 times, most recently from 8376461 to 009b7c8 Compare August 11, 2026 09:20
@ChaoWao

ChaoWao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Re-review of 009b7c86

Re-reviewed against the new merge-base 8a82bf9e. Disclosure: some of this
overlaps a refactor I had prototyped locally, so treat my read of
_log_preload.py / host_span_scope.h / place_binary as less independent than
the rest.

What got better

  • The sink binding is now the right shape. Passing the entry-point address
    from Python (host_span_sink_address → _bind_host_span_sink) beats both
    dlsym(RTLD_DEFAULT) and a weak symbol: Python already holds the CDLL, and
    because the address is passed explicitly there is no load-order constraint —
    binding no longer has to precede the _task_interface import. A stale .so
    lacking the symbol yields AttributeError → address 0 → quiet disable.
  • _host_spans_active() as a function rather than a module constant, so it
    re-evaluates after a later bind. Correct.
  • Field encoding (encode_host_span_field) protects the record framing from
    %, [, ] and non-printables, and keeps space/= only where they are
    grammar.
  • HostLogTest.ForkedProcessesEmitWholePipeRecords turns the fork-atomicity
    invariant from a comment into a test. That one is worth having on its own.
  • The data race I would have raised in enqueue_dispatch is gone: in
    submit_dispatch the admission_mu_ guard covers the whole function and
    trace_run / trace_hash are captured while it is held.

Must fix

1. legacy_spans() is an allow-list and silently drops an existing marker.
simpler_setup/tools/strace_timing.py:142 keeps only simpler_run /
simpler_run.*. simpler_prewarm.build (emitted from
runtime_maker.cpp, reachable via ChipWorker.init(prewarm_config=…)) matches
neither branch, so it disappears from the TPOT table, the tree, the rounds table
and --trace-out — which the PR description says stays unchanged. Reproduced
with a two-record log: the hid=0 bucket present before is absent after.

The intent is to exclude the new family, so invert it to a deny-list on the new
prefix; that is also future-proof for any other marker family.

2. by_name() selects by stream position. strace_timing.py:108 uses
m.setdefault(s.name, s) over self.spans, which is in file order — so "first"
means first in the stream, not earliest by ts. Stream order carries no
information (records are stamped before the write and interleave across threads
and forked children), so this should select by minimum ts.

Latent today, because names happen to be unique within one (pid, inv) — the
device phases come from a static table and task_slot_0…15 are separately
spelled. It stops being latent the moment a per-task family reaches
group_invocations, which is exactly what removing the allow-list in (1) would
do.

3. The parent's log level is never seeded. _initialize_simpler_log has one
caller — python/simpler/task_interface.py:1338, inside ChipWorker.init, which
runs in the forked child. worker.py never calls it, and _log.py binds the sink
at import but never calls simpler_log_init. So the parent's HostLogger stays
at its constructor default TIMING and the Python threshold does not reach it:
setting the simpler logger to NUL does not silence parent-side spans.

Should fix

4. Two atomicity premises in one file. src/common/log/host_log.cpp:45
reasons from _POSIX_PIPE_BUF (512) and budgets span fields to 256 bytes;
:211 says "a record of at most PIPE_BUF bytes reaches a shared pipe
indivisibly" with a 2048-byte buffer. The intent looks deliberate — the portable
512 floor for machine-parsed span records, best-effort for human log lines — but
nothing says so, so a reader cannot tell which bound governs where.

5. _log.py's handler discards a good value.

try:
    from _task_interface import DEFAULT_LOG_THRESHOLD as _NATIVE_DEFAULT
    from _task_interface import _bind_host_span_sink
    _bind_host_span_sink(_host_span_sink_address(_host_log_handle))
except (ImportError, AttributeError):
    _NATIVE_DEFAULT = 25

If the first import succeeds and the second fails — an extension predating
_bind_host_span_sink — the handler overwrites the successfully imported native
constant with the hardcoded 25. It conflates "no extension" with "extension lacks
this symbol".

6. The ordering comment now over-claims. _log.py states the preload is
ordered before the _task_interface import "so the logger entry point is bound
into the extension before any worker can fork". With address passing that
constraint no longer exists; the invariant that actually holds is only "bind
before the first span is emitted".

Consider

7. sink_slot() is per module, not per process. It is a function-local static
in an inline function, and with -fvisibility=hidden on the extension plus
-fno-gnu-unique on the logger each including module gets its own slot. Only
_task_interface and test_scheduler include the header today, so nothing
breaks — but the docstrings say "process-global", which will mislead whoever first
makes host_runtime.so emit a span.

8. The encoder protects record framing, not k=v framing. It runs on the
already-joined attribute string, so it cannot tell a separator space from a space
inside a value: error_message=foo bar encodes unchanged and then parses as
error_message=foo plus a stray bar. Unreachable today (all attributes are
numeric or enum), but the 192-byte cap sits on the same open k=v payload that is
meant to be the extension point, so the first free-text attribute meets both
limits at once.

9. The race fix lengthened a critical section. emit — and
trace_dispatch_attrs's ostringstream — now run while admission_mu_ is held.
Correctness was the right trade; the cheaper shape is to capture the two scalars
under the lock and format/write outside it.

Question

What is the per-record cost on the default build? SIMPLER_HOST_STRACE defaults
on and HostLogger's default threshold is TIMING, so a stock build now pays
roughly four write(2) calls per task serialized on the logger mutex, where every
pre-existing host marker was per chip-run. A before/after on an L3 example with a
non-trivial task count would settle whether that needs addressing before merge.


I have not built or run the suites against this head, so everything above is from
reading the diff plus the surrounding files. Happy to rebuild and run if that
would help.

@TaoZQY

TaoZQY commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@ChaoWao Addressed the review points in the updated single commit:

  • legacy_spans() now denies only the new l3.* namespace, so existing families such as simpler_prewarm.build remain in TPOT/tree/rounds/--trace-out.
  • Invocation.by_name() now selects the minimum timestamp rather than file order.
  • hierarchical Worker.init() seeds the parent logger with the snapshotted Python threshold before its first fork; the regression test pins NUL propagation and call ordering.
  • split the native-default and optional sink-binder imports, and corrected the preload/binding comments.
  • clarified the portable 512-byte machine-record atomicity budget versus the 2048-byte formatting buffer and best-effort human log records.
  • corrected docs/binding text to describe an extension-local/DSO-local sink slot connected to the process-global logger.
  • l3.dispatch captures run/hash while admission is locked, then formats and emits after releasing admission_mu_.

I left item 8 unchanged because all current attributes are numeric/enums; there is no free-text k=v producer yet. The first free-text attribute should introduce value-level encoding rather than encoding an already-joined payload.

Validation: 1340 Python UT passed (6 skipped), 93 non-hardware C++ tests passed, pre-commit passed, and both one-device and two-device L3 a2a3sim scenes passed.

For the performance question, local hardware precheck could not identify the silicon, so I did not bypass it. I compared merge-base 8a82bf9e with this head on the same host using an a2a3sim L3 workload (10 child tasks/round, 2 warmups + 15 measured rounds, 5 fresh processes/version). Median-of-process-medians was 23.28 ms baseline vs 24.04 ms current (+3.3%, about 76.7 us/task), while process medians overlapped broadly: 21.76–27.56 ms baseline and 22.37–26.11 ms current. The paired directions were mixed, so this does not show a stable reproducible regression, but it is noisy host-simulation data rather than a hardware conclusion. A same-device onboard comparison remains the definitive follow-up when an identifiable device is available.

@TaoZQY
TaoZQY force-pushed the feat/issue-1708-host-swimlane branch from 03d3aa0 to 9e72937 Compare August 12, 2026 03:34
Emit graph build, submit, dispatch, frame publication, activation, and
completion spans through the process-global logger. Add a real-pid/tid
Perfetto view with occurrence-matched submit-to-dispatch flows and numeric flow
IDs while preserving every established marker family in the existing views,
selecting repeated phases by timestamp, and keeping unaligned device timestamps
off the host axis.

Load the logger, seed the parent threshold before fork, and bind its C ABI entry
point into the extension-local nullable sink slot, while keeping logger-free
topologies quiet and free of unresolved link symbols. Replace rebuilt shared
libraries by atomic rename so existing mappings remain valid. Capture dispatch
metadata before endpoint publication can retire its slot, then format and emit
outside the admission lock. Percent-encode and bound STRACE records so shared-
pipe writes remain atomic.

Cover the native scheduler stages, Python graph callback, runtime binary
placement, parser conversion, and hostile marker fields with regression tests.

Co-authored-by: Chao Wang <26245345+ChaoWao@users.noreply.github.com>
@TaoZQY

TaoZQY commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@ChaoWao Follow-up with an onboard L3 before/after measurement on the current head.

Setup

  • Baseline: 50c06606 (main at the comparison point)
  • PR: 9e72937e
  • Workload: the existing per_task_runtime_env vector-add kernel under a real Worker(level=3), with 64 independent L2 child submissions per worker.run
  • Same NPU device (9) for every process
  • Order-balanced runs: baseline → PR and PR → baseline
  • Per version/direction: 20 warmups + 50 measured rounds; outputs validated after every process
  • Default host tracing/logging configuration (no tracing or log-level overrides)
  • Both task-submit jobs completed with exit code 0:
    • task_20260812_004308_364535012115
    • task_20260812_004347_3687788815

The architecture precheck could not query npu-smi because DCMI returned -8005; this run therefore explicitly bypassed that precheck and relied on the successful a2a3 workload execution.

Results

Order Build Mean (ms/run) Median (ms/run) Stddev (ms)
baseline → PR baseline 87.518 74.785 49.889
baseline → PR PR 88.711 75.686 54.410
PR → baseline PR 81.728 78.134 12.846
PR → baseline baseline 76.058 75.907 2.752
pooled (100 samples/build) baseline 81.788 75.160 35.792
pooled (100 samples/build) PR 85.219 76.943 39.685

The order-balanced pooled median delta is +1.783 ms/run (+2.37%). With 64 child tasks per run, that is a rough end-to-end estimate of +27.9 us per child task.

The PR logs also confirm the exact marker volume. In each 70-round PR process (20 warmup + 50 measured), they contain:

  • l3.graph_build: 70
  • l3.dispatch: 4,480
  • l3.frame_submit: 4,480
  • l3.complete: 4,480
  • l3.activate: 0

This independent/immediate-dispatch workload therefore emits 193 host-span records per run (1 + 64 * 3), giving a rough measured estimate of 9.24 us per emitted record from the pooled median delta. l3.activate is only emitted for staged/prepare-only activation and is not exercised by this workload, so a four-record staged path was not directly measured; a linear extrapolation from this result would be about 37 us/task, not an observed number.

The means are more outlier-sensitive than the medians here, but both order directions show a positive median delta (+1.2% and +2.9%). So the default-on tracing cost is measurable on this L3 workload, although it is small relative to the full task latency.

The host swimlane named a thread's lane from the first span that thread
emitted. One OS thread emits several roles: the scheduler loop is the sole
caller of both dispatch_ready() and manager->progress(), and within
submit_dispatch the l3.frame_submit scope closes inside submit_progress,
before the l3.dispatch record emitted after the admission lock is released.
Its first span therefore always carried role=worker, so the one lane the
swimlane exists to show was labelled "worker 0". Rank the roles a thread
actually emitted instead, scheduler above worker.

Seed the process logger in _start_hierarchical unconditionally. Gating on
device_ids skipped exactly the pod processes: init() rejects device_ids
above L3, so that branch cannot be taken in an L4 process — which still
drives next-level Workers and still emits their spans. Setting the simpler
logger to NUL could not silence them. A Worker with no chip binaries seeds
the copy the package preloaded at import.

Also:

- Decode the emitter's percent-escapes in strace_timing, so a value that
  needed encoding reads back as its original text rather than as %XX.
- Drop the whole trailing escape when the truncation marker lands on a full
  field, rather than its last byte: %0A became the undecodable %0~.
- Carry parsed attributes alongside their span instead of in a dict keyed on
  id(), which was correct only while the span list stayed alive.
- Scope the host-trace doc to every hierarchical worker rather than to those
  with local chip children, and record that the l3.* names cannot distinguish
  a pod from the L3s under it (hw-native-sys#1793) and that one process contributes at
  most two host lanes.

Cover the scheduler lane naming with the real emission order, the chipless
seeding, percent-decoding, the escape-boundary truncation, and l3.submit's
emit site, whose attributes the dispatch flow arrows pair on.
@ChaoWao
ChaoWao merged commit 9693545 into hw-native-sys:main Aug 13, 2026
19 checks passed
ChaoWao added a commit to ChaoWao/simpler-fork that referenced this pull request Aug 19, 2026
hw-native-sys#1793's C2b reserved `ext.<producer>.<span>` so a caller outside simpler cannot
land a span in one of our level words. hw-native-sys#1877 added the guard that makes the
classification hold — `span_family` answers `external`, and the invocation-keyed
views drop that family. What it did not do is make the rest of the parser stop
treating an external span as a source of truth about our own structure, and
nothing said what a producer may expect in return. Three places read a producer's
record and answered questions about us with it:

  * The swimlane's process label was a two-way split, so a process that emitted
    nothing but external spans was announced as `simpler chip child`.
  * Lane naming reads `role`, which is an ordinary attribute key a producer is
    free to use for its own meaning. `role=facade` on an external span renamed
    its lane to `orchestrator / facade`.
  * Lane splitting reads `slot_id` and `depth`. A pipeline slot is ours, so a
    producer writing one decided whether one of *our* threads split into
    per-slot lanes and into how many.

The invariant behind all three is one sentence: our views infer our structure
only from our own spans. Role inference and lane splitting now filter the family
out, and the process label has a third arm carrying no `simpler` prefix, because
such a process is not ours. Dispatch-flow pairing needed no change — it keys on
`host_span_leaf`, which is `None` for anything not ours, and a test now holds
that.

`external_producer()` names the second segment, and requires all three: a
malformed `ext.foo` is still external, since it can never be mistaken for ours,
but it attributes to nobody rather than to a producer named after whatever
follows. A producer segment may itself be one of our level words — `ext.host.foo`
is a producer called `host` and stays external, which is the case the namespace
was reserved for.

The contract is the deliverable, so it is executable. `tests/ut/py/
test_strace_timing.py` gains eight tests under an `ext.` heading, one per row of
the table now in `docs/dfx/host-trace.md`: the name shape, the impersonation
attempt, exclusion from the invocation views and why (`inv` is our run epoch and
no public surface exposes it, so admitting external spans forges one invocation
holding all of them), presence on the swimlane, the shared-process case, and the
three takeover attempts above. Another repository adapting to this namespace can
read them as the specification and mirror them against its own emitter.

Two names in this subsystem said the wrong thing and are corrected here, since
the change is already rewriting the code and the prose around them.

`legacy_spans` never described what it does. hw-native-sys#1730 introduced it as "spans
belonging to the established `simpler_run` views", so *legacy* qualified the
**views** — the ones that predated the swimlane — and the name reads as though the
spans were obsolete. Its body has since stopped resembling that intent
altogether: it was `not name.startswith("l3.")` and is now a family exclusion, so
the word matches nothing in it. It is `invocation_spans` now, which is what it
returns: the spans an invocation-keyed view may consume. The test asserting it,
and the local holding its result, follow.

This is a name pypto probes with `hasattr(_strace_timing, "legacy_spans")`,
falling back to a `startswith("l3.")` filter when it is absent. That filter is
inert today and stays inert for logs from this tree, because `l3.` was retired in
hw-native-sys#1877 and nothing emits it — so a pypto that has not adopted the new name will
admit host-family spans into its per-launch grouping, and its numbers will be
wrong without an error. That is a known consequence of taking the correct name
here, recorded so the answer is findable from the symptom. Nothing in this
repository reads the old name.

`_legacy_record` in the tests was *accurate* — it builds a record with the
retired wall-clock log prefix rather than `[mono_ns=…]` — but it did not say which
part is old, so it is `_wall_clock_prefix_record`. Renaming it exposed that the
support it exercises had no test: `_HOST_LOG_TIME` accepts both prefix forms, and
dropping the wall-clock arm left all 43 tests green. It is not dead. A record's
attribute list runs to the end of its line, so what bounds it is the lookahead
for the *next* record's log prefix — and two complete records do share one
physical line, often enough that pypto's reader re-splits on `[STRACE]` before
parsing, because ranks forked by an L3 share the capture fd. Without the
wall-clock arm the first record's `attrs` absorbs the second's entire prefix:

    rank=0[2026-08-04 10:00:00.000001][T0x1][TIMING] emit_span: [strace.h:132]

No error, just a wrong value. A test now covers that line shape, and the comment
above the pattern records what makes both arms load-bearing and when the
wall-clock one can go — once no log predating hw-native-sys#1824 is read.

`simpler_setup/tools/README.md` described `--swimlane` as consuming `l3.*`
markers. That spelling was retired by hw-native-sys#1877; the line is in the paragraph this
change rewrites, and the same grep that found the rest of the rename returns it
among a dozen legitimate `l3` variable names, which is how it survived. Other
`legacy` mentions in the tree name genuinely old capture formats and ABI fields
and are left alone.

Verification:

- `pytest tests/ut/py` — 1624 passed, 0 failed; the strace file is 44 of those.
  Two earlier full runs on this same tree failed on
  `test_second_child_failure_reaps_first` and the third did not, which is the
  forked-child reaping flake recorded locally on 2026-08-16. It passes 4/4 in
  isolation, and this change touches no file under `tests/ut/py/test_worker/`
  and nothing in worker startup.
- Each behaviour has a negative control: reverting the process label, the
  role-inference filter, the lane-structure filter, the producer
  well-formedness check, or the wall-clock prefix arm each turns exactly the
  corresponding test red and nothing else.
- End to end through the real emitter rather than a hand-built string:
  `_emit_host_span` writes `chip.run` and `ext.pypto.decode_layer` on one thread
  via `HostLogger::log_host_span`, and the CLI puts the external span on the
  swimlane, keeps it out of `--trace-out` and the TPOT table, and ignores its
  `role=facade slot_id=99`.
- `ctest -LE requires_hardware` — 101/101, unchanged; no C++ in this change.
- ruff, ruff format, pyright, markdownlint clean.
ChaoWao added a commit to ChaoWao/simpler-fork that referenced this pull request Aug 19, 2026
hw-native-sys#1793's C2b reserved `ext.<producer>.<span>` so a caller outside simpler cannot
land a span in one of our level words. hw-native-sys#1877 added the guard that makes the
classification hold — `span_family` answers `external`, and the invocation-keyed
views drop that family. What it did not do is make the rest of the parser stop
treating an external span as a source of truth about our own structure, and
nothing said what a producer may expect in return. Three places read a producer's
record and answered questions about us with it:

  * The swimlane's process label was a two-way split, so a process that emitted
    nothing but external spans was announced as `simpler chip child`.
  * Lane naming reads `role`, which is an ordinary attribute key a producer is
    free to use for its own meaning. `role=facade` on an external span renamed
    its lane to `orchestrator / facade`.
  * Lane splitting reads `slot_id` and `depth`. A pipeline slot is ours, so a
    producer writing one decided whether one of *our* threads split into
    per-slot lanes and into how many.

The invariant behind all three is one sentence: our views infer our structure
only from our own spans. Role inference and lane splitting now filter the family
out, and the process label has a third arm carrying no `simpler` prefix, because
such a process is not ours. Dispatch-flow pairing needed no change — it keys on
`host_span_leaf`, which is `None` for anything not ours, and a test now holds
that.

`external_producer()` names the second segment, and requires all three: a
malformed `ext.foo` is still external, since it can never be mistaken for ours,
but it attributes to nobody rather than to a producer named after whatever
follows. A producer segment may itself be one of our level words — `ext.host.foo`
is a producer called `host` and stays external, which is the case the namespace
was reserved for.

The contract is the deliverable, so it is executable. `tests/ut/py/
test_strace_timing.py` gains eight tests under an `ext.` heading, one per row of
the table now in `docs/dfx/host-trace.md`: the name shape, the impersonation
attempt, exclusion from the invocation views and why (`inv` is our run epoch and
no public surface exposes it, so admitting external spans forges one invocation
holding all of them), presence on the swimlane, the shared-process case, and the
three takeover attempts above. Another repository adapting to this namespace can
read them as the specification and mirror them against its own emitter.

Two names in this subsystem said the wrong thing and are corrected here, since
the change is already rewriting the code and the prose around them.

`legacy_spans` never described what it does. hw-native-sys#1730 introduced it as "spans
belonging to the established `simpler_run` views", so *legacy* qualified the
**views** — the ones that predated the swimlane — and the name reads as though the
spans were obsolete. Its body has since stopped resembling that intent
altogether: it was `not name.startswith("l3.")` and is now a family exclusion, so
the word matches nothing in it. It is `invocation_spans` now, which is what it
returns: the spans an invocation-keyed view may consume. The test asserting it,
and the local holding its result, follow.

This is a name pypto probes with `hasattr(_strace_timing, "legacy_spans")`,
falling back to a `startswith("l3.")` filter when it is absent. That filter is
inert today and stays inert for logs from this tree, because `l3.` was retired in
hw-native-sys#1877 and nothing emits it — so a pypto that has not adopted the new name will
admit host-family spans into its per-launch grouping, and its numbers will be
wrong without an error. That is a known consequence of taking the correct name
here, recorded so the answer is findable from the symptom. Nothing in this
repository reads the old name.

## Reading archived logs is dropped

Two facilities existed only so a log this tree can no longer produce stays
parseable, and the tool is held to one standard instead: the current code is
clean and self-consistent.

  * `_HOST_LOG_TIME` accepted the wall-clock record prefix hw-native-sys#1824 replaced.
  * `_RETIRED_WORDS` mapped `simpler_run` / `simpler_prewarm` / `l3` onto the
    families they used to name. Its own comment tied the two together — "the same
    reason `_STRACE_RE` still accepts the wall-clock record prefix" — so keeping
    one and dropping the other would have left the tool inconsistent with itself.

Both are gone, with the test that guarded the retired spellings and the
`_legacy_record` helper that built the retired prefix. What that costs, measured
rather than assumed, is asymmetric. A retired *chip* name now answers `unknown`,
which `invocation_spans` keeps, so it groups exactly as it did before. A retired
*host* name also answers `unknown` — and `unknown` is kept — so on a log
predating hw-native-sys#1877 the `l3.*` scheduler spans now enter invocation grouping instead
of being excluded from it, which is the pollution the family split exists to
prevent. That is the concrete meaning of no longer reading archived logs.

Removing the wall-clock arm exposed one invariant worth keeping a test for, and
it is not about old logs. A record's attribute list runs to the end of its line,
so what bounds it is the lookahead for the *next* record's log prefix — and two
complete records do share one physical line, because ranks forked by an L3 share
the capture fd, which is why pypto's reader re-splits on `[STRACE]` first. With
the prefix lookahead gone the earlier record's `attrs` absorbs the later one's
whole prefix rather than failing to match:

    rank=0[mono_ns=1000002][T0x2][TIMING] emit_host_span:

No error, just a wrong value, and nothing covered it. A test now does, against
the current prefix.

`test_retired_span_names_...` also asserted `host_span_leaf` on the current names;
that part moved into the level-word test, which is where it belongs.

`simpler_setup/tools/README.md` described `--swimlane` as consuming `l3.*`
markers. That spelling was retired by hw-native-sys#1877; the line is in the paragraph this
change rewrites, and the same grep that found the rest of the rename returns it
among a dozen legitimate `l3` variable names, which is how it survived. Other
`legacy` mentions in the tree name genuinely old capture formats and ABI fields
and are left alone.

Verification:

- `pytest tests/ut/py` — 1624 passed, 0 failed; the strace file is 44 of those.
  Two earlier full runs on this same tree failed on
  `test_second_child_failure_reaps_first` and the third did not, which is the
  forked-child reaping flake recorded locally on 2026-08-16. It passes 4/4 in
  isolation, and this change touches no file under `tests/ut/py/test_worker/`
  and nothing in worker startup.
- Each behaviour has a negative control: reverting the process label, the
  role-inference filter, the lane-structure filter, the producer
  well-formedness check, or the record-bounding prefix lookahead each turns
  exactly the corresponding test red and nothing else.
- End to end through the real emitter rather than a hand-built string:
  `_emit_host_span` writes `chip.run` and `ext.pypto.decode_layer` on one thread
  via `HostLogger::log_host_span`, and the CLI puts the external span on the
  swimlane, keeps it out of `--trace-out` and the TPOT table, and ignores its
  `role=facade slot_id=99`.
- `ctest -LE requires_hardware` — 101/101, unchanged; no C++ in this change.
- ruff, ruff format, pyright, markdownlint clean.
ChaoWao added a commit to ChaoWao/simpler-fork that referenced this pull request Aug 19, 2026
hw-native-sys#1793's C2b reserved `ext.<producer>.<span>` so a caller outside simpler cannot
land a span in one of our level words. hw-native-sys#1877 added the guard that makes the
classification hold — `span_family` answers `external`, and the invocation-keyed
views drop that family. What it did not do is make the rest of the parser stop
treating an external span as a source of truth about our own structure, and
nothing said what a producer may expect in return. Three places read a producer's
record and answered questions about us with it:

  * The swimlane's process label was a two-way split, so a process that emitted
    nothing but external spans was announced as `simpler chip child`.
  * Lane naming reads `role`, which is an ordinary attribute key a producer is
    free to use for its own meaning. `role=facade` on an external span renamed
    its lane to `orchestrator / facade`.
  * Lane splitting reads `slot_id` and `depth`. A pipeline slot is ours, so a
    producer writing one decided whether one of *our* threads split into
    per-slot lanes and into how many.

The invariant behind all three is one sentence: our views infer our structure
only from our own spans. Role inference and lane splitting now filter the family
out, and the process label has a third arm carrying no `simpler` prefix, because
such a process is not ours. Dispatch-flow pairing needed no change — it keys on
`host_span_leaf`, which is `None` for anything not ours, and a test now holds
that.

`external_producer()` names the second segment, and requires all three: a
malformed `ext.foo` is still external, since it can never be mistaken for ours,
but it attributes to nobody rather than to a producer named after whatever
follows. A producer segment may itself be one of our level words — `ext.host.foo`
is a producer called `host` and stays external, which is the case the namespace
was reserved for.

The contract is the deliverable, so it is executable. `tests/ut/py/
test_strace_timing.py` gains eight tests under an `ext.` heading, one per row of
the table now in `docs/dfx/host-trace.md`: the name shape, the impersonation
attempt, exclusion from the invocation views and why (`inv` is our run epoch and
no public surface exposes it, so admitting external spans forges one invocation
holding all of them), presence on the swimlane, the shared-process case, and the
three takeover attempts above. Another repository adapting to this namespace can
read them as the specification and mirror them against its own emitter.

Two names in this subsystem said the wrong thing and are corrected here, since
the change is already rewriting the code and the prose around them.

`legacy_spans` never described what it does. hw-native-sys#1730 introduced it as "spans
belonging to the established `simpler_run` views", so *legacy* qualified the
**views** — the ones that predated the swimlane — and the name reads as though the
spans were obsolete. Its body has since stopped resembling that intent
altogether: it was `not name.startswith("l3.")` and is now a family exclusion, so
the word matches nothing in it. It is `invocation_spans` now, which is what it
returns: the spans an invocation-keyed view may consume. The test asserting it,
and the local holding its result, follow.

This is a name pypto probes with `hasattr(_strace_timing, "legacy_spans")`,
falling back to a `startswith("l3.")` filter when it is absent. That filter is
inert today and stays inert for logs from this tree, because `l3.` was retired in
hw-native-sys#1877 and nothing emits it — so a pypto that has not adopted the new name will
admit host-family spans into its per-launch grouping, and its numbers will be
wrong without an error. That is a known consequence of taking the correct name
here, recorded so the answer is findable from the symptom. Nothing in this
repository reads the old name.

## Reading archived logs is dropped

Two facilities existed only so a log this tree can no longer produce stays
parseable, and the tool is held to one standard instead: the current code is
clean and self-consistent.

  * `_HOST_LOG_TIME` accepted the wall-clock record prefix hw-native-sys#1824 replaced.
  * `_RETIRED_WORDS` mapped `simpler_run` / `simpler_prewarm` / `l3` onto the
    families they used to name. Its own comment tied the two together — "the same
    reason `_STRACE_RE` still accepts the wall-clock record prefix" — so keeping
    one and dropping the other would have left the tool inconsistent with itself.

Both are gone, with the test that guarded the retired spellings and the
`_legacy_record` helper that built the retired prefix. What that costs, measured
rather than assumed, is asymmetric. A retired *chip* name now answers `unknown`,
which `invocation_spans` keeps, so it groups exactly as it did before. A retired
*host* name also answers `unknown` — and `unknown` is kept — so on a log
predating hw-native-sys#1877 the `l3.*` scheduler spans now enter invocation grouping instead
of being excluded from it, which is the pollution the family split exists to
prevent. That is the concrete meaning of no longer reading archived logs.

Removing the wall-clock arm exposed one invariant worth keeping a test for, and
it is not about old logs. A record's attribute list runs to the end of its line,
so what bounds it is the lookahead for the *next* record's log prefix — and two
complete records do share one physical line, because ranks forked by an L3 share
the capture fd, which is why pypto's reader re-splits on `[STRACE]` first. With
the prefix lookahead gone the earlier record's `attrs` absorbs the later one's
whole prefix rather than failing to match:

    rank=0[mono_ns=1000002][T0x2][TIMING] emit_host_span:

No error, just a wrong value, and nothing covered it. A test now does, against
the current prefix.

`test_retired_span_names_...` also asserted `host_span_leaf` on the current names;
that part moved into the level-word test, which is where it belongs.

## The level ladder was written three times and pinned nowhere

Dropping the retired map put weight on what `span_family` does with a word it
does not know, which turned up a defect of my own from hw-native-sys#1877. `_HOST_WORDS`
carried the comment "this parser cannot import the runtime package, so it carries
the list and the unit tests pin the two together" — and **no test referenced
`WorkerLevel` at all**. The level words existed in three independent hand-written
copies: the ladder in `python/simpler/worker_level.py`, this parser's tuple, and a
literal `for word in ("host", "network1", …)` inside the test that was supposed to
be the pin.

So a level added to the ladder would have made the runtime emit a word the parser
does not know, `span_family` would answer `unknown`, and — because unknown is
deliberately kept rather than dropped — those per-task spans would enter
invocation grouping under a forged `(pid, 0)` key. Wrong tables, no error, and
none of the three copies would have complained. That is the real source of an
unrecognized name; a typo is the unlikely one.

`test_every_level_word_the_ladder_names_is_a_word_this_parser_knows` now compares
the two as sets and checks each word reaches the family its ladder position
implies. The parser cannot import the runtime package, but the test can, which is
what makes the pin possible at all. Adding `network4 = 7` to the ladder alone
fails it with `ladder-only=['network4'], parser-only=[]`, and the level-word test
too, since its loop now walks `WorkerLevel` instead of a fourth copy. The comment
that claimed a pin now names the test that is one.

`simpler_setup/tools/README.md` described `--swimlane` as consuming `l3.*`
markers. That spelling was retired by hw-native-sys#1877; the line is in the paragraph this
change rewrites, and the same grep that found the rest of the rename returns it
among a dozen legitimate `l3` variable names, which is how it survived. Other
`legacy` mentions in the tree name genuinely old capture formats and ABI fields
and are left alone.

Verification:

- `pytest tests/ut/py` — 1624 passed, 0 failed; the strace file is 44 of those.
  Two earlier full runs on this same tree failed on
  `test_second_child_failure_reaps_first` and the third did not, which is the
  forked-child reaping flake recorded locally on 2026-08-16. It passes 4/4 in
  isolation, and this change touches no file under `tests/ut/py/test_worker/`
  and nothing in worker startup.
- Each behaviour has a negative control: reverting the process label, the
  role-inference filter, the lane-structure filter, the producer
  well-formedness check, the record-bounding prefix lookahead, or the
  ladder-to-parser pin each turns
  exactly the corresponding test red and nothing else.
- End to end through the real emitter rather than a hand-built string:
  `_emit_host_span` writes `chip.run` and `ext.pypto.decode_layer` on one thread
  via `HostLogger::log_host_span`, and the CLI puts the external span on the
  swimlane, keeps it out of `--trace-out` and the TPOT table, and ignores its
  `role=facade slot_id=99`.
- `ctest -LE requires_hardware` — 101/101, unchanged; no C++ in this change.
- ruff, ruff format, pyright, markdownlint clean.
ChaoWao added a commit that referenced this pull request Aug 19, 2026
#1793's C2b reserved `ext.<producer>.<span>` so a caller outside simpler cannot
land a span in one of our level words. #1877 added the guard that makes the
classification hold — `span_family` answers `external`, and the invocation-keyed
views drop that family. What it did not do is make the rest of the parser stop
treating an external span as a source of truth about our own structure, and
nothing said what a producer may expect in return. Three places read a producer's
record and answered questions about us with it:

  * The swimlane's process label was a two-way split, so a process that emitted
    nothing but external spans was announced as `simpler chip child`.
  * Lane naming reads `role`, which is an ordinary attribute key a producer is
    free to use for its own meaning. `role=facade` on an external span renamed
    its lane to `orchestrator / facade`.
  * Lane splitting reads `slot_id` and `depth`. A pipeline slot is ours, so a
    producer writing one decided whether one of *our* threads split into
    per-slot lanes and into how many.

The invariant behind all three is one sentence: our views infer our structure
only from our own spans. Role inference and lane splitting now filter the family
out, and the process label has a third arm carrying no `simpler` prefix, because
such a process is not ours. Dispatch-flow pairing needed no change — it keys on
`host_span_leaf`, which is `None` for anything not ours, and a test now holds
that.

`external_producer()` names the second segment, and requires all three: a
malformed `ext.foo` is still external, since it can never be mistaken for ours,
but it attributes to nobody rather than to a producer named after whatever
follows. A producer segment may itself be one of our level words — `ext.host.foo`
is a producer called `host` and stays external, which is the case the namespace
was reserved for.

The contract is the deliverable, so it is executable. `tests/ut/py/
test_strace_timing.py` gains eight tests under an `ext.` heading, one per row of
the table now in `docs/dfx/host-trace.md`: the name shape, the impersonation
attempt, exclusion from the invocation views and why (`inv` is our run epoch and
no public surface exposes it, so admitting external spans forges one invocation
holding all of them), presence on the swimlane, the shared-process case, and the
three takeover attempts above. Another repository adapting to this namespace can
read them as the specification and mirror them against its own emitter.

Two names in this subsystem said the wrong thing and are corrected here, since
the change is already rewriting the code and the prose around them.

`legacy_spans` never described what it does. #1730 introduced it as "spans
belonging to the established `simpler_run` views", so *legacy* qualified the
**views** — the ones that predated the swimlane — and the name reads as though the
spans were obsolete. Its body has since stopped resembling that intent
altogether: it was `not name.startswith("l3.")` and is now a family exclusion, so
the word matches nothing in it. It is `invocation_spans` now, which is what it
returns: the spans an invocation-keyed view may consume. The test asserting it,
and the local holding its result, follow.

This is a name pypto probes with `hasattr(_strace_timing, "legacy_spans")`,
falling back to a `startswith("l3.")` filter when it is absent. That filter is
inert today and stays inert for logs from this tree, because `l3.` was retired in
#1877 and nothing emits it — so a pypto that has not adopted the new name will
admit host-family spans into its per-launch grouping, and its numbers will be
wrong without an error. That is a known consequence of taking the correct name
here, recorded so the answer is findable from the symptom. Nothing in this
repository reads the old name.

## Reading archived logs is dropped

Two facilities existed only so a log this tree can no longer produce stays
parseable, and the tool is held to one standard instead: the current code is
clean and self-consistent.

  * `_HOST_LOG_TIME` accepted the wall-clock record prefix #1824 replaced.
  * `_RETIRED_WORDS` mapped `simpler_run` / `simpler_prewarm` / `l3` onto the
    families they used to name. Its own comment tied the two together — "the same
    reason `_STRACE_RE` still accepts the wall-clock record prefix" — so keeping
    one and dropping the other would have left the tool inconsistent with itself.

Both are gone, with the test that guarded the retired spellings and the
`_legacy_record` helper that built the retired prefix. What that costs, measured
rather than assumed, is asymmetric. A retired *chip* name now answers `unknown`,
which `invocation_spans` keeps, so it groups exactly as it did before. A retired
*host* name also answers `unknown` — and `unknown` is kept — so on a log
predating #1877 the `l3.*` scheduler spans now enter invocation grouping instead
of being excluded from it, which is the pollution the family split exists to
prevent. That is the concrete meaning of no longer reading archived logs.

Removing the wall-clock arm exposed one invariant worth keeping a test for, and
it is not about old logs. A record's attribute list runs to the end of its line,
so what bounds it is the lookahead for the *next* record's log prefix — and two
complete records do share one physical line, because ranks forked by an L3 share
the capture fd, which is why pypto's reader re-splits on `[STRACE]` first. With
the prefix lookahead gone the earlier record's `attrs` absorbs the later one's
whole prefix rather than failing to match:

    rank=0[mono_ns=1000002][T0x2][TIMING] emit_host_span:

No error, just a wrong value, and nothing covered it. A test now does, against
the current prefix.

`test_retired_span_names_...` also asserted `host_span_leaf` on the current names;
that part moved into the level-word test, which is where it belongs.

## The level ladder was written three times and pinned nowhere

Dropping the retired map put weight on what `span_family` does with a word it
does not know, which turned up a defect of my own from #1877. `_HOST_WORDS`
carried the comment "this parser cannot import the runtime package, so it carries
the list and the unit tests pin the two together" — and **no test referenced
`WorkerLevel` at all**. The level words existed in three independent hand-written
copies: the ladder in `python/simpler/worker_level.py`, this parser's tuple, and a
literal `for word in ("host", "network1", …)` inside the test that was supposed to
be the pin.

So a level added to the ladder would have made the runtime emit a word the parser
does not know, `span_family` would answer `unknown`, and — because unknown is
deliberately kept rather than dropped — those per-task spans would enter
invocation grouping under a forged `(pid, 0)` key. Wrong tables, no error, and
none of the three copies would have complained. That is the real source of an
unrecognized name; a typo is the unlikely one.

`test_every_level_word_the_ladder_names_is_a_word_this_parser_knows` now compares
the two as sets and checks each word reaches the family its ladder position
implies. The parser cannot import the runtime package, but the test can, which is
what makes the pin possible at all. Adding `network4 = 7` to the ladder alone
fails it with `ladder-only=['network4'], parser-only=[]`, and the level-word test
too, since its loop now walks `WorkerLevel` instead of a fourth copy. The comment
that claimed a pin now names the test that is one.

`simpler_setup/tools/README.md` described `--swimlane` as consuming `l3.*`
markers. That spelling was retired by #1877; the line is in the paragraph this
change rewrites, and the same grep that found the rest of the rename returns it
among a dozen legitimate `l3` variable names, which is how it survived. Other
`legacy` mentions in the tree name genuinely old capture formats and ABI fields
and are left alone.

Verification:

- `pytest tests/ut/py` — 1624 passed, 0 failed; the strace file is 44 of those.
  Two earlier full runs on this same tree failed on
  `test_second_child_failure_reaps_first` and the third did not, which is the
  forked-child reaping flake recorded locally on 2026-08-16. It passes 4/4 in
  isolation, and this change touches no file under `tests/ut/py/test_worker/`
  and nothing in worker startup.
- Each behaviour has a negative control: reverting the process label, the
  role-inference filter, the lane-structure filter, the producer
  well-formedness check, the record-bounding prefix lookahead, or the
  ladder-to-parser pin each turns
  exactly the corresponding test red and nothing else.
- End to end through the real emitter rather than a hand-built string:
  `_emit_host_span` writes `chip.run` and `ext.pypto.decode_layer` on one thread
  via `HostLogger::log_host_span`, and the CLI puts the external span on the
  swimlane, keeps it out of `--trace-out` and the TPOT table, and ignores its
  `role=facade slot_id=99`.
- `ctest -LE requires_hardware` — 101/101, unchanged; no C++ in this change.
- ruff, ruff format, pyright, markdownlint clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants