Skip to content

Fix: gate disabled host spans before formatting - #1875

Open
indigo1973 wants to merge 1 commit into
hw-native-sys:mainfrom
indigo1973:0818
Open

Fix: gate disabled host spans before formatting#1875
indigo1973 wants to merge 1 commit into
hw-native-sys:mainfrom
indigo1973:0818

Conversation

@indigo1973

@indigo1973 indigo1973 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
  • Query live TIMING visibility before clocks, metadata, and attribute work
  • Keep bindable loggers silent until process-state binding
  • Seed standalone logger owners before they emit diagnostics
  • Preserve level-resolved host-scheduler names across runtime gates and docs
  • Cover threshold changes, unbound startup, and disabled scheduler trace work

Addresses items 3 and 7 of #1792.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cc2ecaa4-4de4-43e1-a724-70dfc512cae8

📥 Commits

Reviewing files that changed from the base of the PR and between f4ed104 and e030a55.

📒 Files selected for processing (16)
  • docs/dfx/host-trace.md
  • docs/logging.md
  • python/bindings/task_interface.cpp
  • python/simpler/worker.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
  • src/common/log/unified_log_host.cpp
  • tests/ut/cpp/CMakeLists.txt
  • tests/ut/cpp/a5/test_host_log_off.cpp
  • tests/ut/cpp/common/test_host_log_unbound.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp
  • tests/ut/cpp/stubs/test_stubs.cpp
  • tests/ut/py/test_worker/test_host_worker.py

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Host tracing now checks live runtime and logging thresholds before collecting metadata or emitting spans. Native and Python APIs expose activation state. Scheduler instrumentation and logging tests cover enabled, disabled, and unbound states.

Changes

Host-span gating

Layer / File(s) Summary
Host logging enablement contract
src/common/log/..., docs/logging.md, docs/dfx/host-trace.md
Private loggers now start at NUL. Host-span formatting and emission use early and final TIMING checks.
Runtime activation bridge
python/bindings/task_interface.cpp, python/simpler/worker.py, tests/ut/py/test_worker/test_host_worker.py, tests/ut/cpp/stubs/test_stubs.cpp
Python activation now combines HOST_STRACE_ENABLED with native runtime state. Tests cover short-circuiting and inactive graph instrumentation.
Conditional scheduler instrumentation
src/common/hierarchical/orchestrator.cpp, src/common/hierarchical/worker_manager.cpp
Scheduler paths now skip timestamps, metadata, attributes, and span construction when host tracing is disabled.
Logging and scheduler validation
tests/ut/cpp/CMakeLists.txt, tests/ut/cpp/a5/test_host_log_off.cpp, tests/ut/cpp/common/test_host_log_unbound.cpp, tests/ut/cpp/hierarchical/test_scheduler.cpp
Tests cover threshold queries, silent unbound loggers, disabled span capture, and the new test target wiring.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to e030a

The change gates disabled host spans before formatting and keeps unbound logger instances silent; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant PythonWorker
  participant TaskInterface
  participant HostTrace
  participant Scheduler
  participant HostLogger
  PythonWorker->>TaskInterface: query _host_spans_active()
  TaskInterface->>HostTrace: call enabled()
  HostTrace-->>TaskInterface: return runtime state
  TaskInterface-->>PythonWorker: return combined activation state
  Scheduler->>HostTrace: check enabled()
  HostTrace-->>Scheduler: return enabled or disabled
  Scheduler->>HostLogger: emit span when enabled
  HostLogger-->>Scheduler: suppress work when TIMING is disabled
Loading

Poem

I’m a rabbit who checks every trace,
NUL keeps quiet in its place.
When TIMING calls, spans hop in line,
Disabled paths leave no trace behind.
Tests thump paws: the gates work fine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description directly explains host-span gating, logger behavior, testing, and the related issue.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing formatting work for disabled host spans.

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.

@indigo1973
indigo1973 force-pushed the 0818 branch 2 times, most recently from 1063ee5 to 2d9ccfd Compare August 18, 2026 13:18

@ChaoWao ChaoWao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

审了 eeac80d6...2d9ccfde(16 文件 +273/−53,单 commit,就在 main 顶端)。按 #1792 item 3 / item 7 的原文对,不按 PR 描述对。

结论:gate 的机制是对的,覆盖是完整的,我逐个数过 7 个发射点没有漏的。有一条需要处理的行为回归(见 host_log.cpp 的 inline),加一条缺测量。

gate 的四层,以及为什么两层都必要

unified_log_host_span_enabled() 成为 C ABI 的一等查询 → host_trace::enabled() inline 包装 → 六个 C++ 发射点在构造属性之前查询 → log_host_span 在 validate/encode 之前再查一次

最后那次 recheck 值得单独说:它覆盖的是直接走 C ABI 的调用方 —— pypto 就是这样进来的,它永远看不到 host_span_scope.h 里的 inline gate。只做调用点会把整个外部消费者漏掉。这一层不是冗余。

发射点覆盖(我逐个核过,7/7):orchestrator.cpp:377,737 · worker_manager.cpp:472,662,745,1013 · worker.py:11054SpanScopestd::optional + emplace 是条件构造的唯一办法,terminal_ns 用 0 做哨兵也安全 —— 它唯一的读者就是 release_run 里那个 trace,而且那里同时守了 terminal_ns != 0

还有一处微妙但正确的:_set_host_span_level_prefixWorker.init不受这个 gate 约束worker.py:7800)。所以阈值中途从 NUL 抬到 TIMING 时前缀早已 bind,span 名字仍带正确的层级词。commit message 第三条 bullet 说的就是这个,判断对。

一条 Should-fix:全部理由是一个测量值,却没有测量

item 3 是因为一个数字才从优化升级为必需的:~3.34 µs/record,其中只有约一半能靠降级收回,剩下的是 gate 之前的 encode。这个 PR 把 encode 也纳入 gate,等于主张剩下那一半现在也能收回 —— 但没有任何 before/after 数字。

机制我读过是对的,但"3.34 µs 现在全部可回收"这句话本身还没有证据。一次 a2a3sim 在 TIMING vs WARN 下的对比就够,补进 PR body 即可。

做得对的,值得点名

  • test_host_log_unbound 单独建可执行文件,CMake 注释也写明了原因:singleton 是一次性 bindable 的,test_host_log_off 一 bind 就再也测不到 unbound 状态。这是唯一正确的做法,而且理由留在了代码里。
  • Python 测试用阴性对照而不是输出断言 —— 把 time.monotonic_ns_emit_host_span 都 patch 成抛异常,证明的是"没干活"而不只是"没输出"。同一个测试还验了 STRACE-off build 不会去查 runtime gate(短路顺序对)。
  • test_stubs.cpp 补了 stub,符合本仓 cpput 的拓扑要求 —— 新 extern "C" 符号不补 stub 会挂掉一批 ut target。
  • 文档三处都准确。 我把阈值语义逐条核过:DEBUG/INFO/TIMING → 开,WARN/ERROR/NUL → 关,新测试的表格、is_enabled 的实现、文档措辞三者一致。docs/dfx/host-trace.md 里 "Device logging still uses the initialization-time policy" 尤其值得保留 —— 它对应 #1792 底部记的那个未修缺陷(onboard AICPU 只拿到一次性的 InitArgs.log_level 快照),把"log level 现在是活的"正确地限定在了 host 模块范围内。

一条排序上的事实,不是缺陷

#1792 写过"item 3 应该在 #1793 改名之前落地",但 #1793 先落了(#1877/#1880)。实际代价为零:改名没有增加发射点,而 host_span_name() 的调用现在落在 gate 内侧,关掉时连名字解析都省了 —— 等于额外收益。那条排序建议可以在 issue 里划掉。

Comment thread src/common/log/host_log.cpp
Comment thread src/common/hierarchical/worker_manager.cpp
Comment thread tests/ut/cpp/a5/test_host_log_off.cpp
@indigo1973

indigo1973 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

补充 item 3 的性能验证。

对比环境

  • baseline:upstream/main@93a0fde;current:本 PR 的 item 3 实现。
  • 两侧分别使用全新的 worktree、项目内 venv 和独立构建缓存;PTO-ISA pin 均为 f51c92f6,编译配置一致。

微基准:单个 disabled host span 的调用开销

这是一个针对改动点的原生 C++ 测试,不运行完整模型。测试程序直接使用各自版本的生产 HostLoggerhost_trace::enabled()SpanScope:每次调用先用 std::ostringstream 构造一组与 host dispatch span 同类的属性(run_id/task_slot/group_index/worker_id/dispatch_id/endpoint_kind/prepare_only/role),再构造并析构一个 SpanScope。baseline 无条件执行格式化和 SpanScope;current 在任何时钟、属性格式化和 SpanScope 构造之前先调用 host_trace::enabled(),关闭时立即返回。

程序以 -O3 编译。每种配置先预热 1,000 次;WARN 下每个 trial 调用 1,000,000 次,TIMING 下每个 trial 调用 20,000 次,各做 9 个 trial,统计每次调用耗时的中位数。TIMING 输出重定向到 /dev/null,避免终端显示速度影响对比。反汇编确认 current 的测量循环每轮仍调用 unified_log_host_span_enabled(),测试循环没有被编译器消除。

结果:

  • WARN(span 被关闭):1238.87 ns/span -> 3.34 ns/span,开销降低 99.73%
  • TIMING(span 被开启):2229.13 ns/span -> 2212.65 ns/span,未发现回退。

端到端:真实 L3 Worker.run 路径

测试基于现有场景 tests/st/a2a3/tensormap_and_ringbuffer/test_l3_launch_acceptance.py::TestL3LaunchAcceptance::default,仅增加预热、计时和 A/B 标记,没有替换运行负载。该场景使用单卡、tensormap_and_ringbuffer L3 runtime、4 个 AICPU 线程;输入为三个 float32[128 * 128] 共享内存张量。每个 round 从一次完整的 L3 Worker.run 开始:host graph callable 向 worker 0 提交一个 ChipTask,随后 AICPU orchestration 在嵌套 scope 中提交 5 个 AIV task,计算 (a + b + 1) * (a + b + 2) + (a + b),并等待该轮完成。

每个独立进程从启动时就通过 --log-level warn 初始化日志级别;先执行 10 个相同 round 预热,再对一个包含 170 个 round 的完整场景执行块计时。计时范围包含参数准备/rehost、170 次 Worker.run 及场景的 launch-acceptance 检查,不包含进程启动、编译和前面的 10 个预热 round;使用 --skip-golden 排除 CPU golden 计算与逐轮结果比较。baseline/current 共做 15 组交替顺序的配对测试。a2a3 上板测试在同一个 task-submit 任务、同一块 NPU 上串行完成;a2a3sim 使用相同场景和配对方式。

结果:

  • a2a3sim:配对中位数改善 22.66 us/round-0.60%),15 组中 current 有 8 组更快,结果接近模拟器噪声范围。
  • a2a3:baseline/current 样本中位数分别为 546.99 / 523.00 us/round;配对中位数改善 21.85 us/round-4.05%),20% 截尾均值改善 25.13 us/round-4.59%),15 组中 current 有 13 组更快。测试无错误、重试或 TIMING/STRACE 输出。

结论:微基准直接确认 WARN 下被关闭 span 的格式化开销已基本消除;真实 a2a3 L3 路径也得到稳定的端到端改善。TIMING 开启时未观察到单 span 开销回退。

@ChaoWao ChaoWao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

再审了一轮 93a0fde0...913e33f0,这次拿 .docs/ 里记的需求做对照,不只看 PR 自述。

结论:机制正确、覆盖完整,上轮四条里两条已修。合入前只有一件硬性的事 —— rebase。

上轮四条的处置,我逐个核实过

上轮提的 处置
Must-fix query_device_hal 会永久静音 修得正好
全部理由是一个测量值,却没有测量 未补
enabled() 在帧循环内 未改(thread 仍 open,是对的)
测试留下 NUL 阈值 ✅ 已修

① 值得单独说一句好在哪:main 开头一行 seed,注释写的是 "This executable owns its logger state; no loader binds it." —— 陈述的是 owner 语义这个当前事实,不是"因为 review 说了所以加"。而且 PR body 那条 bullet 写的是 "Seed standalone logger owners"(复数、类别),说明理解的是"永不 bind 的 owner"这一类,而不只是那一个文件。这正是我希望的修法。

一件阻塞项:base 落后 main 17 个 commit

merge-base 是 93a0fde0#1869),而那 17 个 commit 里包含两轮词汇改名#1877l3.*/simpler_run* → 按层级命名)、#1880(L4+ 按网络跳数)、#1886ext. 保留命名空间 + legacy_spansinvocation_spans)、#1893(L3 的层级词 hostnode)。

所以这个 diff 里的 span 名全是旧词汇:

  • orchestrator.cpp / worker_manager.cpp 里的 HostSpan:: 用法本身没问题(名字是运行时拼的),但
  • test_scheduler.cpp 的 9 处断言host.dispatch / host.complete 等字面量 → main 上现在是 node.*
  • docs/dfx/host-trace.md 的改动基于 <level> = host(L3) 那一版

好消息是这些会冲突而不是静默错#1893 改的是同一批行),所以 rebase 时会响亮地告诉你。但 rebase 完要重跑 ctest,因为 test_scheduler.cpp 断言的正是那些 span 名。

三条来自 .docs/ 对照的发现

1. .docs 要的是 enabled(level),实现给的是 enabled() —— 见 inline。

2. item 7 在 .docs/ 里没有需求条目。 我 grep 了 trace-and-log-requirements.mdlog-design.md:unbound / pre-bind 默认这件事只有一处间接提及(作为另一条的症状)。也就是说 item 7 是 #1845 落地之后才出现的新问题,.docs 的六条 statement 里没有它的位置。这不是这个 PR 的问题,是 .docs 该补的一行 —— 记在这里只是因为它解释了为什么 item 7 的设计依据比 item 3 薄:它没有一份写在前面的需求可以对。

3. .docs/log-design.md 的 statement 4 预判了 item 7 的来源。 它写的是:

The only thing that must be shared is the state (threshold plus a drop counter): pass a pointer to it when we load each module

item 7 正是这个设计的直接后果 —— 一旦 state 按指针共享,"绑定之前用什么"就成了必须回答的问题,而 statement 4 没有回答它。所以 item 7 不是独立缺陷,是 item 4 的未决尾巴。这条因果值得在某处留下,否则下一个人会以为它是偶发的。

还剩的两条 nit

补一次 a2a3sim 在 TIMING vs WARN 下的对比就够。item 3 是因为一个数字(~3.34 µs/record,其中约一半在 gate 之前)才从优化升级为必需的;这个 PR 把 encode 也纳入 gate,等于主张剩下那一半现在也能收回 —— 值得有个数字支撑,而不是只有机制。

enabled() 提到循环外,和 submit_dispatch / finish_progress_dispatch 已有的"取一次复用"形状保持一致。

两条都不阻塞。rebase 之后我可以再跑一遍确认。

Comment thread src/common/log/include/common/host_span.h
@ChaoWao

ChaoWao commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

更正我上一条 review 里的一句判断 —— 我说错了,而且方向是低估。

我写的是"这些会冲突而不是静默错,所以是响亮的"。核实后不成立:GitHub 报 MERGEABLE,而且它是对的。这个 PR 在 test_scheduler.cpp新增的两行断言不在 #1893 改过的行上,所以 rebase 不冲突。

真实后果比冲突和"测试红"都糟 —— 是假绿

// 913e33f0, test_scheduler.cpp:1258-1259
EXPECT_FALSE(captured_host_span("host.dispatch"));
EXPECT_FALSE(captured_host_span("host.complete"));

captured_host_span(name) 就是"捕获列表里有没有这个名字"(:74-78)。main 上现在发的是 node.dispatch / node.complete#1893),所以:

  • captured_host_span("host.dispatch") → 永远找不到 → false
  • EXPECT_FALSE(false)通过

于是 DisabledHostSpansSkipDispatchAndCompletionEmission绿,但什么都没验证。它是 item 3 那个 gate 在 C++ 侧唯一的守护,而 merge 之后无论 gate 是否工作它都通过。

严重性排序,我上一条把它放在了最轻的一档:

形态 何时发现
冲突 立刻
测试红 CI
假绿(本例) 永不

让它静默的是 EXPECT_FALSE + 过期名字这个组合。如果那两行是 EXPECT_TRUE,改名后会立刻红 —— 断言的否定形式把"名字过期"从响亮变成了静默。

建议的修法:不要只改名字

rebase 后把 host. 换成 node. 能修好眼前这次,但这个断言形式本身是脆的 —— 下一次改名它会再次 vacuously true。这个测试要证明的是"gate 关掉后一个 span 都不发",那就直接断言这件事,和名字脱钩:

// 关掉 gate 后不该有任何捕获,无论叫什么
EXPECT_TRUE(captured_host_span_names.empty());

captured_host_span_names 就在同文件 :56ScopedHostSpanCapture 每次构造时清空,所以这个断言是精确的。)

这样它比原来更强:原来只排除两个特定名字,新形式排除全部;而且任何改名都不能让它假绿。

附带一点

#1886 加的梯子钉子(test_every_level_word_the_ladder_names_is_a_word_this_parser_knows钉不住这个 —— 它比对的是 WorkerLevel 和解析器的 _NODE_WORDS,管不到 C++ 测试里的 span 名字面量。所以"改名有钉子守着"这个说法要限定范围:Python 解析器侧有,C++ 测试断言侧没有。这个案例是我知道的第一个具体实例。

ChaoWao added a commit that referenced this pull request Aug 20, 2026
)

Reviewing #1875 turned up how a rename can go silently wrong. That PR adds

    EXPECT_FALSE(captured_host_span("host.dispatch"));

on a base that predates #1893, where the word became `node`. Rebasing produces no
conflict — the line is new, not edited — so the assertion looks for a name nothing
emits, `captured_host_span` answers false, and `EXPECT_FALSE(false)` **passes**.
The test that exists to prove a disabled gate emits nothing would then pass
whether the gate works or not.

Ranked by when you find out, that is the worst of the three outcomes a rename can
have: a conflict is immediate, a red test is one CI run, and a vacuously-true
assertion is never. What makes it silent is the negated form — the same staleness
under `EXPECT_TRUE` merely goes red.

Two changes, and the first is not a new guard but the removal of what made the
guard necessary.

**The names come from the table the emitter uses.** Eight literals in
`test_scheduler.cpp` are now `host_span_name(HostSpan::Dispatch)` and friends.
These tests assert that a decision point emitted at all; the name is not their
subject, so writing it out was a second copy of it. Taking it from the single
source means a rename cannot leave them behind — verified by renaming the level
word to `network9` and re-running: the suite stays green because the assertions
follow. Under the old literals the same rename would have reddened the
`EXPECT_TRUE` ones and silently passed an `EXPECT_FALSE` one.

**The C++ pre-bind default is pinned to the ladder.** `host_span_names.h`'s
`level_word()` defaults to `"node"`, which was a third hand-written copy of the L3
word: `WorkerLevel` is the source of truth, `strace_timing.py`'s `_NODE_WORDS` is
pinned to it by its own test, and this one was pinned to nothing. A level renamed
in Python would leave C++ emitting the old word for every span before a Worker
binds the prefix, with no test noticing.

The pin reads that default through the existing binding rather than adding a
symbol: `set_level_prefix("")` returns early without binding, while the binding
still reports what is in effect. It runs in a child process because the prefix
freezes on first bind, so a Worker constructed anywhere in this process would
leave the *bound* word behind instead of the default.

`test_graph_failure_still_emits_graph_build_span` also spelled out
`node.graph_build`; it now builds the expected name from the worker's own
`_host_span_prefix`, which is the contract that assertion is about.

Verification:

- `pytest tests/ut/py` — 1657 passed, 0 failed.
- `ctest -LE requires_hardware` — 107/107.
- Negative control for the pin: setting the C++ default back to `"host"` fails
  `test_the_cpp_pre_bind_level_word_is_the_ladder_word_for_l3` and nothing else.
- Negative control for the double-write removal: renaming the level word to
  `network9` keeps `test_scheduler` green, where a literal would have gone red or,
  worse, vacuously true.

Not every literal span name is a double-write, and the distinction is which side
of the test it sits on. A literal that **constructs input** — the
`SimplerHostSpan` fed to the logger in `test_host_log_off.cpp`, the fake log lines
in `test_strace_timing.py` — is right to be written out: the test owns its input,
and it is exercising encoding or parsing over an arbitrary name. A literal that
**asserts the implementation emitted a particular name** is the second copy, and
that is the class both changes above remove. The eight in `test_scheduler.cpp`
were the only ones of that kind.

What this does not cover, stated because "renames are guarded now" would be too
broad a claim: the pin ties the C++ *default* word to the ladder, not every place
a name could be spelled out. `git grep -n '"\(node\|network[123]\)\.'` before
landing a rename, and judge each hit by the input-versus-assertion test above.
- Query live TIMING visibility before clocks, metadata, and attribute work
- Keep bindable loggers silent until process-state binding
- Seed standalone logger owners before they emit diagnostics
- Preserve level-resolved host-scheduler names across runtime gates and docs
- Cover threshold changes, unbound startup, and disabled scheduler trace work

Addresses items 3 and 7 of hw-native-sys#1792.
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.

2 participants