Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .claude/rules/codestyle.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,35 @@

3. Prefer `volatile` decorator on struct members rather than volatile pointer casts unless necessary.
4. Avoid using pointer arithmetic with hardcoded offsets when `offsetof` is available.
5. **Never use `std::this_thread::yield()` or `sched_yield()` in AICPU spin-wait loops.** On the Ascend AICPU, yielding to the OS scheduler introduces unacceptable latency for tight spin-waits (ticket locks, CAS retries, etc.). Use an empty loop body or a bare architecture hint (`__asm__ volatile("yield")`) instead.
5. **No sleeping on the dispatch path — at any tier.** A wait that a task's
latency passes through may only ever *spin* or *block on a wakeup
primitive* (semaphore, futex, condition variable, pipe/eventfd read).
`sleep`, `yield`, and backoff timers are all forbidden there, on the host
as much as on the AICPU: a sleep quantum is added to every dispatch that
lands mid-quantum, and it is not recoverable by tuning — the sleep
interval on any general-purpose OS overshoots what you asked for.

**Initialization and teardown paths are exempt** and may poll with a
sleep — a startup handshake or a shutdown drain costs a one-off wait,
not per-task latency. `_STARTUP_POLL_INTERVAL_S` in `worker.py` is the
sanctioned shape.

The boundary is *"does a task wait behind this?"*, not which file it is
in. A mailbox poll waiting for `TASK_READY`, a completion poll waiting
for `TASK_DONE`, a ready-queue wait, an AICPU ticket lock or CAS retry:
all dispatch path. Registration, device bring-up, `init()` readiness
barriers, `close()` reaping: all exempt.

On the AICPU specifically, **never** `std::this_thread::yield()` or
`sched_yield()` — yielding to the OS scheduler costs more than the wait
it replaces. Use an empty loop body or a bare architecture hint
(`__asm__ volatile("yield")`).

When an idle spin is genuinely too expensive to leave running (a forked
child that would hold a core for its whole lifetime), the answer is a
blocking wakeup primitive, **not** a sleep — blocking costs no CPU and
wakes in single-digit microseconds, where a sleep-based compromise pays
latency on every dispatch to save CPU while idle.
6. **For cross-platform/platform-isolation preprocessor blocks, place the `__aarch64__` branch first.** Use this ordering pattern:

```cpp
Expand Down
26 changes: 12 additions & 14 deletions src/common/hierarchical/worker_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,8 @@ std::string format_digest(const uint8_t *digest) {
return out;
}

// Task-dispatch spin iterations between child liveness samples. Paired with
// the loop's 50 us sleep this is a ~10 ms sampling period.
constexpr int kChildLivenessPollInterval = 200;

// Wall-clock period between child liveness samples on spin loops that do not
// sleep between iterations.
// Wall-clock period between child liveness samples. Every mailbox wait spins,
// so an iteration count would not map to a bounded wall time.
constexpr std::chrono::milliseconds kChildLivenessPollPeriod{10};

std::string child_status_message(int child_pid, int status) {
Expand Down Expand Up @@ -368,22 +364,24 @@ WorkerCompletion LocalMailboxEndpoint::run(Ring *ring, const WorkerDispatch &dis
// Signal child process.
write_mailbox_state(MailboxState::TASK_READY);

// Spin-poll until child signals TASK_DONE. A child that dies without
// publishing TASK_DONE would otherwise leave this loop spinning forever,
// so its liveness is sampled every kChildLivenessPollInterval iterations
// (~10 ms at the 50 us sleep below), amortizing the waitpid() syscall.
int poll_count = 0;
// Spin-poll until child signals TASK_DONE. The task's latency runs through
// this wait, so it never sleeps (codestyle rule 5). A child that dies
// without publishing TASK_DONE would otherwise leave the loop spinning
// forever, so its liveness is sampled on a kChildLivenessPollPeriod wall
// clock rather than an iteration count, which no longer maps to a wall
// time once the loop runs at spin speed.
auto next_liveness_check = std::chrono::steady_clock::now() + kChildLivenessPollPeriod;
while (read_mailbox_state() != MailboxState::TASK_DONE) {
if (++poll_count >= kChildLivenessPollInterval) {
poll_count = 0;
auto now = std::chrono::steady_clock::now();
if (now >= next_liveness_check) {
next_liveness_check = now + kChildLivenessPollPeriod;
std::string death = check_child_death();
if (!death.empty()) {
completion.outcome = EndpointOutcome::ENDPOINT_FAILURE;
completion.error_message = "LocalMailboxEndpoint::run: " + death;
return completion;
}
}
std::this_thread::sleep_for(std::chrono::microseconds(50));
}

// Inspect the child's error report before releasing the mailbox back
Expand Down
Loading