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
27 changes: 27 additions & 0 deletions python/bindings/worker_bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,33 @@ inline void bind_worker(nb::module_ &m) {
m.attr("MAILBOX_FRAME_SIZE") = static_cast<int>(MAILBOX_FRAME_SIZE);
m.attr("MAILBOX_OFF_ERROR_MSG") = static_cast<int>(MAILBOX_OFF_ERROR_MSG);
m.attr("MAILBOX_ERROR_MSG_SIZE") = static_cast<int>(MAILBOX_ERROR_MSG_SIZE);
// The MailboxState values as the C++ side defines them, keyed by
// enumerator name. They are a cross-process wire contract: the word at
// MAILBOX_OFF_STATE is written by a parent and read by its forked child,
// so a Python constant that disagrees with this table is a protocol
// mismatch between two live processes, not a compile error. simpler.worker
// declares its own constants for use in hot paths and checks them against
// this table at import.
nb::dict mailbox_states;
mailbox_states["IDLE"] = static_cast<int32_t>(MailboxState::IDLE);
mailbox_states["TASK_READY"] = static_cast<int32_t>(MailboxState::TASK_READY);
mailbox_states["TASK_DONE"] = static_cast<int32_t>(MailboxState::TASK_DONE);
mailbox_states["SHUTDOWN"] = static_cast<int32_t>(MailboxState::SHUTDOWN);
mailbox_states["CONTROL_REQUEST"] = static_cast<int32_t>(MailboxState::CONTROL_REQUEST);
mailbox_states["CONTROL_DONE"] = static_cast<int32_t>(MailboxState::CONTROL_DONE);
mailbox_states["INIT_READY"] = static_cast<int32_t>(MailboxState::INIT_READY);
mailbox_states["INIT_FAILED"] = static_cast<int32_t>(MailboxState::INIT_FAILED);
mailbox_states["FRAME_STAGED"] = static_cast<int32_t>(MailboxState::FRAME_STAGED);
mailbox_states["TASK_LAUNCHED"] = static_cast<int32_t>(MailboxState::TASK_LAUNCHED);
mailbox_states["TASK_FAILED"] = static_cast<int32_t>(MailboxState::TASK_FAILED);
mailbox_states["ACTIVATE"] = static_cast<int32_t>(MailboxState::ACTIVATE);
mailbox_states["PREPARE_READY"] = static_cast<int32_t>(MailboxState::PREPARE_READY);
m.attr("MAILBOX_STATE_VALUES") = mailbox_states;
nb::dict mailbox_dispositions;
mailbox_dispositions["NONE"] = static_cast<int32_t>(MailboxPreparationDisposition::NONE);
mailbox_dispositions["VALIDATED_ONLY"] = static_cast<int32_t>(MailboxPreparationDisposition::VALIDATED_ONLY);
mailbox_dispositions["NATIVE_PREPARED"] = static_cast<int32_t>(MailboxPreparationDisposition::NATIVE_PREPARED);
m.attr("MAILBOX_PREPARATION_DISPOSITION_VALUES") = mailbox_dispositions;
m.attr("PTO_PIPELINE_MAX_DEPTH") = static_cast<uint32_t>(PTO_PIPELINE_MAX_DEPTH);
m.attr("MAX_RING_DEPTH") = static_cast<int32_t>(MAX_RING_DEPTH);
m.attr("MAX_SCOPE_DEPTH") = static_cast<int32_t>(MAX_SCOPE_DEPTH);
Expand Down
4 changes: 4 additions & 0 deletions python/simpler/task_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
MAILBOX_ERROR_MSG_SIZE,
MAILBOX_FRAME_SIZE,
MAILBOX_OFF_ERROR_MSG,
MAILBOX_PREPARATION_DISPOSITION_VALUES,
MAILBOX_SIZE,
MAILBOX_STATE_VALUES,
MAX_REGISTERED_CALLABLE_IDS,
MAX_TENSOR_DIMS,
ArgDirection,
Expand Down Expand Up @@ -170,6 +172,8 @@ def _assert_bindings_match_source_tree() -> None:
"MAILBOX_FRAME_SIZE",
"MAILBOX_OFF_ERROR_MSG",
"MAILBOX_ERROR_MSG_SIZE",
"MAILBOX_STATE_VALUES",
"MAILBOX_PREPARATION_DISPOSITION_VALUES",
"read_args_from_blob",
# Dynamic CommDomain allocation (orch-only API)
"CommBufferSpec",
Expand Down
68 changes: 68 additions & 0 deletions python/simpler/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ def my_l4_orch(orch, args, config):
MAILBOX_ERROR_MSG_SIZE,
MAILBOX_FRAME_SIZE,
MAILBOX_OFF_ERROR_MSG,
MAILBOX_PREPARATION_DISPOSITION_VALUES,
MAILBOX_SIZE,
MAILBOX_STATE_VALUES,
CallConfig,
ChipCallable,
ChipDomainContext,
Expand Down Expand Up @@ -286,6 +288,10 @@ def my_l4_orch(orch, args, config):
_OFF_ACCEPTED = MAILBOX_FRAME_SIZE - MAILBOX_ERROR_MSG_SIZE - 8
_TASK_ACCEPTED = 1
_OFF_PREPARATION_DISPOSITION = _OFF_ACCEPTED + 4
# The parent resets the disposition word to NONE when it claims a frame, so a
# child never publishes it: staging reports VALIDATED_ONLY or NATIVE_PREPARED
# and nothing else. Declared so the wire check below covers the whole enum.
_DISPOSITION_NONE = 0
_VALIDATED_ONLY = 1
_NATIVE_PREPARED = 2
_OFF_FRAME_PROTOCOL = _OFF_ACCEPTED - 40
Expand Down Expand Up @@ -330,6 +336,68 @@ def my_l4_orch(orch, args, config):
_TASK_FRAME_COUNT = 2


def _assert_mailbox_wire_constants() -> None:
"""Fail import if these constants disagree with the C++ enums.

The state word and the disposition word are a cross-process contract: a
parent writes them and its forked child reads them, so a value that differs
from the C++ side is a protocol mismatch between two live processes. Nothing
else catches it — the two declarations are in different languages, so there
is no compile error, and a wrong value reads as a legal-but-different state
rather than as corruption. The constants stay literals because they are read
on the mailbox polling path; this checks them instead of replacing them.
"""
declared = {
"IDLE": _IDLE,
"TASK_READY": _TASK_READY,
"TASK_DONE": _TASK_DONE,
"SHUTDOWN": _SHUTDOWN,
"CONTROL_REQUEST": _CONTROL_REQUEST,
"CONTROL_DONE": _CONTROL_DONE,
"INIT_READY": _INIT_READY,
"INIT_FAILED": _INIT_FAILED,
"FRAME_STAGED": _FRAME_STAGED,
"TASK_LAUNCHED": _TASK_LAUNCHED,
"TASK_FAILED": _TASK_FAILED,
"ACTIVATE": _ACTIVATE,
"PREPARE_READY": _PREPARE_READY,
}
dispositions = {
"NONE": _DISPOSITION_NONE,
"VALIDATED_ONLY": _VALIDATED_ONLY,
"NATIVE_PREPARED": _NATIVE_PREPARED,
}
for name, table, native in (
("MailboxState", declared, MAILBOX_STATE_VALUES),
("MailboxPreparationDisposition", dispositions, MAILBOX_PREPARATION_DISPOSITION_VALUES),
):
# Report every disagreement at once: a renumbering usually moves several
# values, and fixing them one import at a time is needless.
mismatched = sorted(
f"{key}: python={value} c++={native[key]}"
for key, value in table.items()
if key not in native or native[key] != value
)
if mismatched:
raise RuntimeError(
f"{name} constants in simpler.worker disagree with the C++ enum: "
+ "; ".join(mismatched)
+ ". These cross a process boundary, so the mismatch would surface as a hung or "
"misrouted child rather than an error."
)
# A value the C++ side has and Python does not is a state a child can
# legitimately publish and this module would not recognise.
missing = sorted(set(native) - set(table))
if missing:
raise RuntimeError(
f"{name} has enumerators the Python side does not declare: {', '.join(missing)}. "
"A child can publish them and this module would not recognise the value."
)


_assert_mailbox_wire_constants()


def _local_task_frame_count(platform: str, _runtime: str, pipeline_depth: int) -> int:
if platform == "a2a3" and pipeline_depth >= 2:
return _TASK_FRAME_COUNT
Expand Down
70 changes: 70 additions & 0 deletions tests/ut/py/test_worker/test_mailbox_atomics.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,73 @@ def orch(o, args, cfg):
finally:
counter_shm.close()
counter_shm.unlink()


class TestWireConstantAgreement:
"""The mailbox state and disposition words are a cross-process contract.

A parent writes them and its forked child reads them, so a Python constant
that disagrees with the C++ enum is a protocol mismatch between two live
processes: the value is legal, just different, so it surfaces as a hung or
misrouted child rather than as an error. The two declarations are in
different languages, so no compiler catches it.
"""

def test_every_declared_state_matches_the_native_enum(self):
from _task_interface import MAILBOX_STATE_VALUES # noqa: PLC0415
from simpler import worker as worker_mod # noqa: PLC0415

declared = {
"IDLE": worker_mod._IDLE,
"TASK_READY": worker_mod._TASK_READY,
"TASK_DONE": worker_mod._TASK_DONE,
"SHUTDOWN": worker_mod._SHUTDOWN,
"CONTROL_REQUEST": worker_mod._CONTROL_REQUEST,
"CONTROL_DONE": worker_mod._CONTROL_DONE,
"INIT_READY": worker_mod._INIT_READY,
"INIT_FAILED": worker_mod._INIT_FAILED,
"FRAME_STAGED": worker_mod._FRAME_STAGED,
"TASK_LAUNCHED": worker_mod._TASK_LAUNCHED,
"TASK_FAILED": worker_mod._TASK_FAILED,
"ACTIVATE": worker_mod._ACTIVATE,
"PREPARE_READY": worker_mod._PREPARE_READY,
}
assert declared == dict(MAILBOX_STATE_VALUES)

def test_every_declared_disposition_matches_the_native_enum(self):
from _task_interface import MAILBOX_PREPARATION_DISPOSITION_VALUES # noqa: PLC0415
from simpler import worker as worker_mod # noqa: PLC0415

declared = {
"NONE": worker_mod._DISPOSITION_NONE,
"VALIDATED_ONLY": worker_mod._VALIDATED_ONLY,
"NATIVE_PREPARED": worker_mod._NATIVE_PREPARED,
}
assert declared == dict(MAILBOX_PREPARATION_DISPOSITION_VALUES)

def test_the_guard_rejects_a_renumbered_constant(self):
"""The import-time check fails on a divergence rather than passing it through."""
from simpler import worker as worker_mod # noqa: PLC0415

original = worker_mod._TASK_LAUNCHED
worker_mod._TASK_LAUNCHED = original + 100
try:
with pytest.raises(RuntimeError, match="disagree with the C"):
worker_mod._assert_mailbox_wire_constants()
finally:
worker_mod._TASK_LAUNCHED = original
# Restoring is what lets the rest of the suite keep running.
worker_mod._assert_mailbox_wire_constants()

def test_the_guard_rejects_an_undeclared_enumerator(self):
"""A state the child can publish but Python does not know is also a failure."""
from simpler import worker as worker_mod # noqa: PLC0415

original = worker_mod.MAILBOX_STATE_VALUES
worker_mod.MAILBOX_STATE_VALUES = {**dict(original), "FUTURE_STATE": 13}
try:
with pytest.raises(RuntimeError, match="does not declare"):
worker_mod._assert_mailbox_wire_constants()
finally:
worker_mod.MAILBOX_STATE_VALUES = original
worker_mod._assert_mailbox_wire_constants()
Loading