Skip to content

Fix: count a pending accept only for a slot that reaches an endpoint - #1776

Merged
ChaoWao merged 1 commit into
mainfrom
fix-1556-poisoned-accepts
Aug 11, 2026
Merged

Fix: count a pending accept only for a slot that reaches an endpoint#1776
ChaoWao merged 1 commit into
mainfrom
fix-1556-poisoned-accepts

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Orchestrator::submit_task raised the run's accept count before deciding whether the slot was poisoned by a failed producer. A poisoned slot is marked failed and consumed without ever being dispatched, so mark_task_accepted — the sole matching decrement (orchestrator.cpp:517) — is never called for it. pending_accepts then stays above zero for the rest of the run.

The run still finishes. acceptance_ready() is submission_closed && (pending == 0 || is_terminal(phase)), so the terminal disjunct eventually satisfies it. What is lost is when the fence opens.

Why that matters more now than when #1556 was filed. The fence opens at completion instead of at acceptance, and Worker.submit() waits on the prior handle's acceptance before building the next graph. So one poisoned producer silently serialises the next submission behind the entire run — removing exactly the overlap the acceptance fence exists to provide. #1556 was filed on 2026-07-29, before W1b/W1c; the direct-chip pipeline that landed since is built on that same overlap, so the cost is now higher than the issue describes.

Fixes #1556.

The change

Move the increment past the poisoned branch, so only a slot certain to reach an endpoint owes an accept. Nothing else moves.

Two things I checked before trusting that:

  • No other exit sits between the old and new positions. The only return in between is the poisoned one — the path that must skip the count. (The catch at :820 is upstream of both.)
  • The increment does not need run->completion_mu, unlike decrement_run_accepts, which takes it specifically so a decrement cannot land between a waiter's predicate check and its block. acceptance_ready() requires submission_closed, which cannot be true while submit_task is still admitting into that run — so there is no waiter to race.

Testing

The regression test asserts the fence opens while the run is still live, with a healthy sibling owing the only accept that can still arrive.

Verified it fails against the previous ordering, not just that it passes now — I reverted the fix, rebuilt, and got exactly the assertion this bug predicts:

test_orchestrator.cpp:258: Failure
Value of: orch.run_accepted(run_id)
  Actual: false
Expected: true
the poisoned slot is still owing an accept it can never receive

One non-obvious constraint the test had to respect: the sibling must be submitted before the producer is failed, because a run that has recorded an error rethrows it from the next submit_next_level (orchestrator.cpp:706) rather than admitting anything more. My first draft submitted it after and died with C++ exception with description "producer failed".

Suite Result
C++ UT (ctest -LE requires_hardware) 92/92
Python UT 1311 passed / 13 skipped
a2a3 onboard sweep (task-submit, device lock held) 56 passed + 24 passed / 2 skipped, 0 failures

The onboard sweep is included because this changes the orchestrator's admission path, which every dispatched task crosses.

@coderabbitai

coderabbitai Bot commented Aug 11, 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: 12f4fca3-8d5f-4523-9824-705f7198e3be

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
📝 Walkthrough

Walkthrough

The submission path now increments pending acceptance counts only after successful publication. Failed producer propagation leaves poisoned consumers uncounted. A regression test verifies that the acceptance fence opens without waiting for the poisoned consumer.

Changes

Acceptance accounting

Layer / File(s) Summary
Count only successfully published slots
src/common/hierarchical/orchestrator.cpp, tests/ut/cpp/hierarchical/test_orchestrator.cpp
submit_impl increments pending acceptance counts after successful publication. The regression test verifies that a poisoned consumer does not keep the run pending acceptance.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Poem

A rabbit watched the poisoned slot,
And found its count was rightly not.
The live task crossed the fence with cheer,
No endless waiting lingered here.
Hop, hop—acceptance is clear!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: count pending accepts only for slots that reach an endpoint.
Description check ✅ Passed The description directly explains the pending-accept bug, the fix, the regression test, and validation results.
Linked Issues check ✅ Passed The change implements issue #1556 by excluding producer-poisoned slots from pending accepts and adding the required regression test.
Out of Scope Changes check ✅ Passed The source change and regression test directly support the linked issue and stated pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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: 1

🤖 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 `@src/common/hierarchical/orchestrator.cpp`:
- Around line 899-906: Update the acceptance accounting around
increment_run_accepts() in submit_impl() to close the race with
Scheduler::poison_task(): synchronize acceptance registration with failure
claims, or compensate any already-published slots that fail before registration.
Ensure every increment_run_accepts() has exactly one matching
mark_task_accepted() and prevent pending_accepts from remaining elevated after a
poisoned slot.
🪄 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: b44a688a-e060-4115-9b40-4f9ddc5d5d59

📥 Commits

Reviewing files that changed from the base of the PR and between 89f15eb and 9762ef4.

📒 Files selected for processing (2)
  • src/common/hierarchical/orchestrator.cpp
  • tests/ut/cpp/hierarchical/test_orchestrator.cpp

Comment thread src/common/hierarchical/orchestrator.cpp Outdated
Orchestrator::submit_task raised the run's accept count before deciding whether
the slot was poisoned by a failed producer. A poisoned slot is marked failed and
consumed without ever being dispatched, so mark_task_accepted -- the sole
matching decrement -- is never called for it, and pending_accepts stays above
zero for the rest of the run.

The run still finishes: acceptance_ready() is submission_closed && (pending ==
0 || is_terminal(phase)), so the terminal disjunct eventually satisfies it. What
is lost is when the fence opens. It opens at completion instead of at
acceptance, and Worker.submit() waits on the prior handle's acceptance before
building the next graph, so a single poisoned producer serialises the next
submission behind the whole run -- removing exactly the overlap the acceptance
fence exists to provide, and with it the overlap the direct-chip pipeline was
built for.

The count now happens inside the publication lock, in the branch whose
compare-exchange moves the slot out of BUILDING. That branch is the only moment
at which the slot is certain to reach an endpoint, and both sides of it are
wrong: counting earlier includes slots that fail instead, and counting after the
lock is released leaves a window in which a published slot is already visible to
Scheduler::poison_task, which can claim and fail it before the count lands --
the same permanently-elevated count in a narrower race. pending_accepts is
incremented directly rather than through increment_run_accepts, whose get_run()
would take runs_mu_ while this thread holds fanout_mu; that left
increment_run_accepts with no callers, so it is removed.

The regression test asserts the fence opens while the run is still live, with a
healthy sibling owing the only accept that can still arrive; it fails against
either wrong ordering with "Actual: false" on exactly that assertion. The
sibling has to be submitted before the producer is failed, because a run that
has recorded an error rethrows it from the next submit_next_level rather than
admitting anything more.

Fixes #1556.

Verification: 92/92 C++ unit tests (ctest -LE requires_hardware), 1311 Python
unit tests, and the a2a3 onboard sweep at 56 passed plus 24 passed / 2 skipped
in the resource phase with zero failures.
@ChaoWao
ChaoWao force-pushed the fix-1556-poisoned-accepts branch from 9762ef4 to ed1b4ec Compare August 11, 2026 07:15
@ChaoWao

ChaoWao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai 那条 inline 成立并已修,细节写在 thread 上。摘要:

它指出的窗口是本 PR 新引入的main 上计数在发布之前(:833 vs :850),我把它移到发布之后以修 #1556,结果换来一个更窄的同类竞态 —— slot 已发布为 PENDING,Scheduler::poison_task 可以认领并失败它,而计数尚未落地。两侧都错,正确位置是发布锁内、compare_exchange_strong 成功那一支。

连带清理:新写法不再经 increment_run_accepts(它的 get_run() 会在持有 fanout_mu 时取 runs_mu_,是新锁序),该函数因此零调用者,已连同声明删除。

本轮按要求只处理 comments,未动 CI。已 rebase 到 4b13b69c 并重新验证:92/92 cpp · 1311 Python · a2a3 硬件 56 + 24/2 skipped 零失败。

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Changed files =='
git diff --name-only HEAD~1..HEAD 2>/dev/null || true

echo '== Orchestrator outline =='
ast-grep outline src/common/hierarchical/orchestrator.cpp --items all --match 'submit|increment_run_accepts|poison' 2>/dev/null || true

echo '== submit implementation context =='
rg -n -C 35 'compare_exchange_strong|pending_accepts|increment_run_accepts|fanout_mu|poison' \
  src/common/hierarchical/orchestrator.cpp

echo '== Remaining increment_run_accepts references =='
rg -n -C 2 '\bincrement_run_accepts\b' . \
  -g '!**/.git/**' || true

echo '== Regression-test context =='
rg -n -C 25 'pending_accepts|acceptance fence|poisoned|producer.*fail|failed producer' \
  tests/ut/cpp/hierarchical/test_orchestrator.cpp

Length of output: 41040


@ChaoWao 已验证修复。

pending_accepts 现在在 s.fanout_mu 锁内递增,并且只在 compare_exchange_strong 成功发布 READYPENDING 后执行。这样 Scheduler::poison_task 不会在 slot 已发布但计数未建立的窗口中失败该 slot。

失败生产者路径和 CAS 失败路径均不会递增该计数。increment_run_accepts 已无剩余引用。回归测试也验证了 poisoned consumer 不会阻塞仍处于活动状态的 run 的 acceptance fence。


✏️ Learnings added
Learnt from: ChaoWao
URL: https://github.com/hw-native-sys/simpler/pull/1776

Timestamp: 2026-08-11T07:17:19.400Z
Learning: In `src/common/hierarchical/orchestrator.cpp`, `RunState::pending_accepts` must be incremented while holding the submitted `TaskSlotState::fanout_mu` and only after the successful `TaskState::BUILDING` publication `compare_exchange_strong`. This prevents `Scheduler::poison_task` from failing a published slot before its matching pending-accept count exists. Do not call a helper that acquires `runs_mu_` while holding `fanout_mu`; `submit_impl` already has the resolved `RunState` pointer.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@ChaoWao
ChaoWao merged commit 144319b into main Aug 11, 2026
34 of 35 checks passed
@ChaoWao
ChaoWao deleted the fix-1556-poisoned-accepts branch August 11, 2026 08:24
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.

Producer-poisoned slots inflate pending_accepts, delaying the run acceptance fence to completion

1 participant