This document defines callable identity for local hierarchical workers.
hashid is the stable identity across the parent process and its local child
worker processes. Integer execution slots are target-worker internals and are
not exposed through public APIs, task slots, or mailbox task frames.
Related background:
callable-ipc-dynamic-register.md is a historical note for the earlier
cid-based IPC design; the hashid identity contract in this document is the
source of truth. This document does not depend on remote-l3-worker-design.md.
Remote workers are outside the scope of this document.
Hierarchical callable registration needs to handle two cases that a public integer callable id cannot model safely:
- A higher-level task may depend on lower-level callables that are only known
at run time. For example, a parent orchestration callable may depend on a
child
ChipCallable. - Worker-local execution slots may differ across child workers. A parent or orchestration function must not depend on those slot values being inherited or consistent.
The local worker design is register-before-dispatch:
bootstrap workers
register callable identities
install target-local digest -> slot mappings
prewarm chip executable state when applicable
dispatch tasks by callable handle
target worker resolves handle hashid to its local slot
Runtime demand-fetch is not part of the local worker contract. A child never pauses task execution to ask its parent for a missing callable binary; registration must materialize the target-local identity mapping before dispatch. For chip targets, the local implementation also prewarms executable state before first use when startup or dynamic register control reaches the child before dispatch.
The current chip child loop preserves the historical TASK_READY lazy-prepare safety net: if a digest is already registered in the child identity table but the explicit prewarm step was missed, the child prepares its private slot in the dispatch path and logs a warning. This is not a public cid compatibility path and it does not fetch missing callable bytes from the parent.
Chip callable registration separates identity installation from executable
materialization. A successful registration installs the target-local
hashid -> local_slot mapping. For runtimes that need device-side executable
state before the first dispatch, register_callable() also prewarms that state
before returning success.
For tensormap_and_ringbuffer, this prepare-ahead step runs a private AICPU
entry, simpler_aicpu_register_callable, after the orchestration SO bytes and
child kernel addresses have already been staged. The host passes a small
RegisterCallableArgs descriptor (callable id, orch-SO device address/size,
entry/config symbol names) extracted from the staged CallableState — not a
full Runtime. The AICPU entry may materialize the orchestration SO into a
temporary file, dlopen it, resolve the entry/config/bind symbols, and
populate orch_so_table_[callable_id]. It must
stop before any real task execution: it does not call the orchestration entry,
does not configure runtime arguments from user task inputs, does not create a
PTO2Runtime, does not enter runtime scopes, and does not submit scheduler or
AICore work. dlopen may still run ELF or C++ static constructors; the
contract is that prewarm does not explicitly invoke orchestration logic.
host_build_graph does not support AICPU callable prewarm. Its orchestration
SO is resolved on the host during prepare, so there is no AICPU
orch_so_table_ entry to populate and no AICPU dlopen accounting to update.
The internal prewarm hook for that runtime is a no-op success.
Host-visible cache-hit state is committed only after the device-side SO load
has succeeded. Until that commit happens, the next real run must treat the
callable as load-required rather than advertising an AICPU cache hit. If
prewarm fails, register_callable() fails and rolls back the callable
registration it just created. If a first real run performs the same load as a
fallback, it follows the same success-before-commit rule. Reusing a
callable_id slot after unregister forces the next prewarm or real run to
reload and replace the AICPU table entry for that slot.
aicpu_dlopen_count is host telemetry, not proof that the AICPU completed
dlopen. Validation must also rely on an AICPU-side signal, such as a log from
the SO-load helper, or on a follow-up real run that reuses a non-empty
orch_so_table_[callable_id] with reload disabled.
Introduce hashid as the stable callable identity:
hashid = sha256:<64 lowercase hex characters>
target_namespace names the target registry domain that can resolve a
CallableHandle. It is scoped to the Worker instance that returned the
handle. It is not globally unique, not user-supplied, and not part of
hashid.
The namespace answers "which resolver owns this handle?" It is not a raw
worker id, not an affinity selector, and not by itself a platform/runtime
identity. Platform and runtime compatibility for chip callables is encoded in
the CHIP_CALLABLE descriptor fields and revalidated by the target.
Local namespace values:
| Namespace | Callable kind | Resolver domain |
|---|---|---|
LOCAL_CHIP |
CHIP_CALLABLE |
Chip execution registry. |
LOCAL_PYTHON |
PYTHON_SERIALIZED |
Python dispatch registry. |
LOCAL_CHIP is used for an L2 ChipWorker, direct L3 chip children created
from device_ids, and descendant chip registries reached by the existing
recursive ChipCallable cascade. All targets in one LOCAL_CHIP install must
accept the descriptor's target_arch, platform, and runtime. If a future
Worker supports heterogeneous chip targets, incompatible groups must use
separate namespaces or fail registration.
LOCAL_PYTHON is used for Python-capable child loops owned by this Worker:
SUB children and next-level Worker-child dispatch loops. It updates the
parent-owned Python dispatch registry for those children. It does not populate
an inner Worker's own _callable_registry; callables owned by an inner Worker
are registered on that inner Worker separately.
Worker.register derives target_namespace from the target object and the
Worker topology:
ChipCallabletargets produceLOCAL_CHIP.- Python callable targets produce
LOCAL_PYTHON.
Submit APIs validate that the selected execution path can resolve the handle's
namespace. For example, submit_next_level on an L3 Worker with chip children
requires LOCAL_CHIP, while submit_next_level on an L4 Worker child path
requires a Python dispatch handle. The receiving child mailbox derives its
resolver namespace from the selected child path: chip child loops resolve
against LOCAL_CHIP, and SUB children plus next-level Worker-child Python loops
resolve against LOCAL_PYTHON. Local task frames carry only the raw sha256
digest because each current child path has exactly one resolver domain.
Each target namespace owns a private mapping:
identity_table:
hashid -> local_slot
slot_table:
local_slot -> executable callable state
Rules:
hashidis valid across the parent and local child workers.local_slotis valid only inside the target namespace that allocated it.- Public APIs and task records carry
CallableHandle, not local slots. - Local mailbox task frames carry the raw 32-byte digest, not
hashidstrings or slots. - A parent, scheduler,
Orchestrator, or user orchestration function must not know a child worker'shashid -> local_slotmapping. - Registering the same
hashidwith a different descriptor or payload digest is an error. - Public
Worker.registercalls are not deduplicated. Repeated calls may return distinctCallableHandleobjects with the samehashid. - A target may deduplicate executable state by
hashidinternally, but it must preserve independent public handle lifetimes with a target-local refcount.
hashid is computed over canonical descriptor bytes:
hashid = sha256(canonical_descriptor_bytes)
The canonical byte stream is versioned and deterministic:
- All integers are unsigned little-endian.
- Strings are UTF-8 encoded as
uint32 byte_lenfollowed by bytes. - Byte arrays are encoded as
uint32 byte_lenfollowed by bytes. - Lists are encoded as
uint32 countfollowed by items in descriptor order. - Map-like data is not allowed in descriptors.
- Optional fields use
uint8 presentfollowed by the value when present. - Enum fields use fixed
uint32values defined by the descriptor schema. - Every descriptor starts with
uint32 descriptor_schema_versionanduint32 callable_kind.
Descriptor enum values:
| Field | Value | Meaning |
|---|---|---|
callable_kind |
1 |
CHIP_CALLABLE |
callable_kind |
2 |
PYTHON_SERIALIZED |
The implementation uses internal helpers for descriptor construction and hashing:
chip_descriptor = build_chip_callable_descriptor(
target=chip_callable,
platform=platform,
runtime=runtime,
)
python_descriptor = build_python_serialized_descriptor(serialized_payload)
chip_hashid = compute_callable_hashid(chip_descriptor)
python_hashid = compute_callable_hashid(python_descriptor)Worker.register computes the descriptor and hashid. User code does not
provide a hashid override.
Canonical descriptor schemas:
CHIP_CALLABLE:
descriptor_schema_version
callable_kind
target_arch
platform
runtime
callable_blob_sha256: uint8[32]
signature_schema_hash: uint8[32]
PYTHON_SERIALIZED:
descriptor_schema_version
callable_kind
payload_format_version
serializer_id
payload_sha256: uint8[32]
For CHIP_CALLABLE:
target_archis the architecture directory selected from the platform, such asa2a3ora5.platformis the Worker's configured platform id, such asa2a3sim,a2a3,a5sim, ora5.runtimeis the Worker's configured runtime name.callable_blob_sha256is computed over the exact contiguousChipCallablebytes addressed bybuffer_ptr()andbuffer_size().signature_schema_hashis the semantic digest of the public chip entry signature, defined below.
CHIP_CALLABLE identity deliberately excludes build provenance and component
digests from the canonical descriptor:
callable_blob_sha256covers the contiguousChipCallablepayload, including fixed header fields, the public signature, the orchestration binary, and embedded child kernel binaries. Separateorch_so_sha256andkernel_binary_sha256[]values would be redundant for identity. They may be recorded as diagnostic or cache metadata outside the canonical hash.compiler_idandcompiler_versiondescribe how bytes were produced, not the bytes that will execute. Including them would make byte-identical callables produce different hashids.runtime_abi_versionis not part of this schema because the repository does not define a stable runtime ABI version constant that targets can validate.target_arch,platform,runtime, and payload validation are the local execution gates.
Any schema change must use a new descriptor_schema_version rather than
changing this schema in place.
signature_schema_hash is encoded as the raw 32-byte SHA-256 digest in the
descriptor. Logs and diagnostics may render it as sha256:<64 hex>. The
digest input uses the same canonical primitive rules as descriptors:
CHIP_SIGNATURE_SCHEMA_V1:
uint32 signature_schema_version = 1
uint32 sig_count
uint32 arg_direction[sig_count]
arg_direction values are the stable ArgDirection enum values:
| Value | Meaning |
|---|---|
0 |
SCALAR |
1 |
IN |
2 |
OUT |
3 |
INOUT |
The signature hash covers ChipCallable.signature_[0:sig_count_] and
sig_count_ only. It does not cover task-time tensor shapes, data types,
runtime CallConfig, child CoreCallable signatures, function names, or
executable bytes. Those executable and structural bytes are covered by
callable_blob_sha256. Targets must reject a descriptor whose
signature_schema_hash does not match the signature decoded from the
ChipCallable blob.
PYTHON_SERIALIZED covers the existing serialized Python callable route,
including importable functions, lambdas, closures, nested functions, and
callable objects supported by the serializer. Its identity is the serialized
payload identity:
payload_format_versionandserializer_ididentify the existing Python callable payload envelope and serializer.payload_sha256is computed over the exact serializer output bytes. ForCLOUDPICKLE, these are the bytes returned bycloudpickle.dumps(target), not theSPYCenvelope header.
payload_format_version is the Python callable wire payload version from
python-callable-serialization.md. The
current value is 1, matching the SPYC header version. Targets validate that
the staged payload header agrees with the descriptor fields before installing
the callable.
serializer_id uses the same value space as the serializer field in the
SPYC Python callable payload header defined by
python-callable-serialization.md:
| Value | Name | Serializer output |
|---|---|---|
1 |
CLOUDPICKLE |
cloudpickle.dumps(target) bytes |
This document does not define a separate serializer registry.
PYTHON_SERIALIZED hashids are not semantic Python-code identities. Recreating
an equivalent lambda or closure and serializing it again may produce different
payload bytes and therefore a different hashid. This is acceptable: the handle
identifies the concrete serialized callable payload that was registered.
Registration returns a callable handle, not an integer child slot:
handle = Worker.register(callable)Registration uses whole-scope install. A successful Worker.register means
every active child endpoint in the handle's target_namespace for this
Worker has installed the callable identity. Registering to a user-selected
worker subset is not part of this contract.
orch.submit_next_level(..., worker=...) and
orch.submit_next_level_group(..., workers=...) are submit-time affinity
controls; they do not define registration scope. NEXT_LEVEL affinity consumes
stable worker ids. Local Python Worker children and remote L3 workers use the
worker ids returned by add_worker(...) / add_remote_worker(...); L3 chip
worker ids are the existing chip worker ids.
CallableHandle is the public callable token returned by registration:
CallableHandle(
hashid: str,
kind: Literal["CHIP_CALLABLE", "PYTHON_SERIALIZED"],
target_namespace: str,
)The handle is an opaque parent-side registration object. Its hashid is the
stable callable identity used in task frames, but repeated registrations of
the same callable may return distinct handle objects with the same hashid.
Unregistering one handle must not invalidate another live handle that shares
the same hashid.
The parent Worker tracks live handle ids separately from the target-local
ref_count. This keeps public handle lifetime independent from target-side
deduplication: unregistering one live handle consumes exactly that handle and
only releases target-local state when the ref_count reaches zero.
Constructing a CallableHandle directly from its public fields does not create
a live registration. Submit and unregister paths must validate that the handle
belongs to the Worker that returned it and that its public fields still match
the parent-side live registration record.
Submit APIs accept only CallableHandle:
matmul = worker.register(chip_callable)
postprocess = worker.register(py_callable)
def parent_orch(orch, args, config):
orch.submit_next_level(matmul, args, config)
orch.submit_sub(postprocess, args)They do not accept bare strings or raw callables. Direct string hashids are registration internals, not submit arguments.
This contract covers the Worker, Orchestrator, and public ChipWorker
wrapper APIs. Manual callable_id slots are private implementation details
inside the worker and the underscore _ChipWorker binding ABI. They are not
valid CallableHandle values, orchestration submit arguments, task slots, or
mailbox task callable references.
Top-level Worker.run keeps the current behavior:
- L2 runs a registered
CallableHandle. - L3+ runs the raw Python orchestration function in the parent process.
The L3+ orchestration function captures CallableHandle values and passes
them to orch.submit_next_level or orch.submit_sub. Hashid does not add a
new top-level registration requirement for Worker.run.
Each target namespace records local identity state. In the current local mailbox implementation this state is intentionally compact:
identity_table:
hashid -> local_slot
identity_refs:
hashid -> ref_count
slot_table:
local_slot -> {
hashid,
callable_kind,
executable_state,
}
local_slot is private to the target process. It may appear in local debug
logs, but it must not appear in public handles, parent-side task slots, or
control replies.
hashid itself is content-derived and is not reused for a different callable.
Registering the same hashid with a different descriptor or payload is always a
mismatch error.
Repeated public registrations of the same descriptor increment the target-local
ref_count for that hashid. Unregistering one handle decrements the
refcount; the target removes local resolution and frees executable state only
when the refcount reaches zero.
Current local slot reuse rule:
- A child resolves
hashid -> local_slotimmediately before execution. - Each endpoint has one local mailbox operation in flight at a time.
- Parent-side dispatch and control operations to the same endpoint are serialized by the per-WorkerThread mailbox lock.
- Final unregister removes the hashid from resolution and releases the private slot only after the current mailbox operation has completed.
This rule prevents stale slot reuse without exposing any extra public field.
A future remote or multi-flight control channel must add explicit
INSTALLED / TOMBSTONED / FAILED target states, sequence numbers, and
in-flight user draining before it can reuse private slots safely.
Registration remains synchronous and whole-scope. For a given
target_namespace, the scope is every active child endpoint in the current
Worker's corresponding resolver domain at register start.
- Parent builds the canonical descriptor and computes the
hashid. - Parent allocates an unpublished parent-side registration entry and handle id, but does not expose it to user code yet.
- Parent sends
REGISTER_CALLABLEto every target in the scope. - Target validates descriptor bytes, payload digest, feature gates, and namespace.
- Target installs
hashid -> local_slot, or incrementsref_countwhen the same descriptor and payload are already installed. - Parent returns the
CallableHandleonly after every target in the scope reports success.
If any target fails or times out:
- The parent does not return the handle.
- The parent removes the unpublished handle entry.
- The parent sends cleanup to targets that may have installed the hashid.
- If cleanup cannot be confirmed, that target/hashid pair is marked uncertain and must not be used again by the current Worker. The current implementation has no recovery path other than Worker restart. The local broadcast path returns per-target status; parent-side cleanup sends the reverse unregister only to targets that confirmed install or refcount increment. A cleanup failure on one of those targets marks the hashid uncertain conservatively.
This is failure cleanup with conservative uncertainty handling. The current
implementation treats _uncertain_hashids as an intentional unrecoverable
terminal state for the Worker lifetime: a marked digest is blocked until the
Worker restarts. Recovery by successful retry, timeout expiry, or fail-loud
Worker teardown is deferred to a later design.
Parent-side scheduling assumes the handle's hashid is installed on every
active target in its registration scope. Dispatch choices are constrained by
the handle namespace, submit-time affinity, and tensor/buffer accessibility.
Submit-time live validation is a preflight check only. It does not pin the
target identity through later drain or child dispatch. Callers must not
concurrently unregister a handle while Worker.run() or any in-flight task may
submit or use that handle; wait for the relevant run/drain to return before
unregistering it.
Parent-side TaskSlotState stores the submitted callable's stable identity:
the 32-byte sha256 digest plus parent-side scheduling metadata such as
callable kind and target namespace. It never stores a child-local slot, cid,
local handle id, or any other integer callable identity. Local mailbox task
frames carry the fixed sha256 digest.
The target child loop owns the final execution resolve:
TASK(target_namespace, hashid, args)
target namespace identity_table[hashid] -> local_slot
execution engine run(local_slot, args)
ChipWorker may keep an integer slot API internally, but that integer is an
implementation detail of the target child process.
If a required handle namespace or hashid mapping is missing, fail before dispatching work to lower-level children. Runtime demand-fetch is not part of the local worker contract.
Required error codes:
HASHID_FORMAT_INVALID
HASHID_DESCRIPTOR_MISMATCH
CALLABLE_KIND_UNSUPPORTED
LOCAL_SLOT_EXHAUSTED
REGISTER_PARTIAL_FAILURE
REGISTER_CLEANUP_UNCERTAIN
REGISTER_TOMBSTONE_ACTIVE
UNREGISTER_TOMBSTONE_ACTIVE
Error messages should include worker id, namespace, hashid, and operation.
The current local mailbox has one outstanding operation per endpoint and does
not carry a separate sequence field; a future multi-flight control channel must
add one. Messages must not include user-specific local absolute paths.
Register-family controls carry identity and materialization data, not slots. The current local IPC encoding is compact because targets can reconstruct the canonical descriptor from the staged materialization payload and their local execution context:
callable_hash_digest: uint8[32]
payload:
staged POSIX shm reference for register
absent for unregister
Rules:
- Register requests never carry a requested slot.
- Register replies never expose target-local slots.
target_namespaceandcallable_kindare determined by the selected worker pool and control sub-command, not by a user-provided slot.- Targets reconstruct or parse the staged payload and recompute the canonical descriptor digest locally before install. If a future callable kind cannot reconstruct its canonical descriptor from the staged payload and local context, its control request must carry descriptor bytes explicitly.
REGISTER_CALLABLEfor an already-installed matching hashid increments the target-localref_count.REGISTER_CALLABLEfor an already-installed hashid with different descriptor or payload digest fails withHASHID_DESCRIPTOR_MISMATCH.UNREGISTER_CALLABLEidentifies entries bytarget_namespaceandhashidand decrements the target-localref_count.- A reply that returns a different
hashidthan requested is invalid.
Local mailbox task callable reference:
MAILBOX_OFF_CALLABLE:
reserved uint64 = 0
MAILBOX_OFF_ARGS:
callable_hash_digest: uint8[32]
task_args_blob: bytes
The local mailbox task frame is fixed to sha256 and carries only the raw
32-byte digest. It does not carry target_namespace; the receiving child
mailbox determines the target namespace and resolves the digest in its own
identity_table.
This task frame layout is a clean-break wire-format change from the prior
integer callable-id layout where MAILBOX_OFF_CALLABLE carried the callable
id. Parent and child processes must use the same mailbox layout version. There
is no compatibility migration path in the current local fork model. The
task-args capacity is computed after the digest prefix, so task_args_blob
capacity is reduced by 32 bytes relative to a layout without the prefix.
Unregister uses hashid as the target-local primary key:
UNREGISTER_CALLABLE(target_namespace, hashid)
Target unregister sequence:
- Decrement the target-local refcount for
hashid. - If the refcount remains nonzero, keep the mapping installed.
- If the refcount reaches zero, stop new local resolutions from
hashidto private slot. - Clear executable state.
- Release the private slot for reuse.
- Remove or archive the
hashidentry.
This sequence is the concrete unregister form of the target-local slot reuse rule for the current single-flight local mailbox. A future multi-flight target must insert a tombstone/drain phase before clearing executable state.
If failed-register cleanup cannot be confirmed, the parent must not dispatch that hashid to the uncertain target again during the current Worker lifetime. This protects target-local slot cleanup only; it does not allow the same hashid to name a different descriptor or payload.
The implementation provides canonical descriptor and hash helpers:
build_chip_callable_descriptorandbuild_python_serialized_descriptorproduce canonical descriptor bytes forChipCallableandPYTHON_SERIALIZED.compute_callable_hashidreturns the publicsha256:<hex>identity.- Descriptor mismatch and malformed descriptor paths are covered by tests.
The public API is handle-based:
Worker.registerreturnsCallableHandle.CallableHandlevalidation rejects forged, stale, mutated, or wrong-namespace handles.- L3+
Worker.run(raw_orch_fn, ...)behavior is unchanged. - Integer execution slots remain private to the target child process.
Each target owns identity state:
- The target-local
identity_tablemapshashid -> local_slot. - Target child processes install
hashid -> local_slotmappings. - Duplicate public registrations increment target-local refcounts.
- Control replies do not expose slots.
Submit and task records carry identities, not slots:
- Python
Orchestratorsubmit APIs accept onlyCallableHandle. - Nanobind and C++
Orchestratorsignatures carry digest, callable kind, and target namespace. TaskSlotStatestores the 32-byte digest plus scheduling metadata instead of integer callable ids.
Local mailbox task frames are hashid-based:
- The local mailbox task payload is prefixed with the 32-byte
sha256digest. - The existing
TaskArgsblob follows the digest prefix. - Chip and sub child loops resolve
hashid -> local_slotimmediately before execution. ChipWorker.run(local_slot)remains private to the child process.
Register failure cleanup is conservative:
- Handles are not published until every target in scope installed the hashid.
- Failed register removes the unpublished parent handle entry.
- Targets that may have installed the hashid receive reverse cleanup.
- Cleanup uncertainty marks the hashid unavailable for the Worker lifetime.
Required tests:
| Test | Expected result |
|---|---|
| Stable descriptor hash | Same descriptor bytes produce same hashid. |
| Descriptor mismatch | Rejected with HASHID_DESCRIPTOR_MISMATCH. |
| No public slot | Public APIs never return child-local slots. |
| L3 run unchanged | Worker.run(raw_orch_fn, ...) still works at L3+. |
| Submit handle only | Submit rejects bare strings and raw callables. |
| Task frame digest | Local mailbox task frames carry the raw 32-byte digest. |
| Private slot resolve | Child loop resolves hashid to private slot. |
| Slot independence | Same hashid runs with different private slots. |
| Duplicate register | Repeated register returns independent handles. |
| Target refcount | Duplicate same-hashid installs share target state safely. |
| Whole-scope register | All active targets in scope installed. |
| Post-start register | Run-time register succeeds after child start. |
| Pre-start register | Startup hashid mappings are visible after ready. |
| Partial register failure | No public handle is returned. |
| Cleanup uncertainty | Unconfirmed cleanup blocks that target/hashid pair. |
| Unregister cleanup | Hashid resolution stops before final slot cleanup. |
| Unsupported kind | Target rejects unsupported kind before install. |
| Hashid format fuzz | Bad prefix, length, or hex encoding is rejected. |
| No slot consistency | Workers do not need matching private slots. |