How-to: run on multiple chips¶
One host driving several chips is L3. The single-chip API you already know still applies; L3 adds a host-side orchestration function that submits one task per chip, plus optional Python "sub worker" callables for post-processing.
Worked examples live in
examples/workers/l3/, whose README
enumerates what each one demonstrates.
The lifecycle¶
from simpler.worker import Worker
worker = Worker(
level=3,
platform="a2a3sim",
runtime="tensormap_and_ringbuffer",
device_ids=[0, 1], # one chip child process per entry
num_sub_workers=1, # optional host-side Python callables
)
# Register everything BEFORE init(): chip callables and any sub-worker callable.
chip_handle = worker.register(chip_callable)
postprocess = worker.register(lambda args: print("post-process got", args))
worker.init() # forks the chip children and sub children, starts the scheduler
def my_orch(orch, args, cfg):
# Plain Python, running in the host process — not a C++ kernel.
orch.submit_next_level(chip_handle, chip_args, worker=0) # target a specific chip
orch.submit_sub(postprocess, sub_args) # schedule host-side work
try:
worker.run(my_orch, args, config)
finally:
worker.close() # shuts the children down and releases shared memory
Three differences from L2 that trip people up:
device_ids(plural), notdevice_id. Each entry becomes its own forked child process holding its own chip.- The orchestration function is Python and runs on the host. At L2 the
orchestration is a compiled
.cpp; at L3 the top-level orchestration is a Python callable that hands compiledChipCallables down to the children. - Register before
init().init()is the fork point, so registration afterwards does not reach children that already exist.
To direct a task at one specific child rather than any free one, see Directed NEXT_LEVEL Scheduling.
Collectives across chips¶
Cross-chip collectives run through a communication domain: a symmetric memory window that every participating rank can address. Allocate it inside the orchestration function, as a context manager, and index it by domain-local rank:
def orch_fn(orch, _args, cfg):
with orch.allocate_domain(
name="default",
workers=list(range(nranks)),
window_size=window_size,
buffers=[CommBufferSpec(name="scratch", dtype="float32",
count=elems, nbytes=scratch_nbytes)],
) as handle:
for i in range(nranks):
domain = handle[i] # domain-local rank i
... # build per-rank args, submit per chip
A worker can belong to more than one domain, and each domain gets its own window slice. Full semantics — allocation, lifetime, the rank map, and what happens when you ask for a domain a worker is not in — are in Communication Domains.
The shipped collectives are hand-written AIV kernels that compute peer pointers
from the window, not HCCL collective calls; HCCL is used only to bootstrap the
domain. The algorithm corpus (allreduce in several modes, allgather,
reduce_scatter, broadcast, all_to_all) lives in
tests/st/worker/collectives/ — read those scene tests for the real
implementations.
Scope limits worth knowing up front¶
- Collectives are single-host, multi-card. Both backends state this explicitly; there is no cross-node collective path today.
--enable-l2-swimlanedoes not work on L3. It is rejected up front. To get a swimlane, scope the run to one chip with--level 2.- Multi-host (L4) is not usable for data movement yet. The control plane is real — you can register a remote worker, and the remote side builds a genuine L3 subtree — but the transport is simulation-backed, and the daemon rejects any other setting. The RoCE / HCCS / UB profiles are documented contracts, not working paths. See Remote L3 Worker Design and the Capability Survey before designing against it.
Running¶
# simulation, two virtual chips
python examples/workers/l3/multi_chip_dispatch/main.py -p a2a3sim -d 0,1
# hardware
pytest tests/st/worker/collectives --platform a2a3 --device 4-7
On shared machines, hardware runs should hold the devices they use rather than racing other users for them; see Testing for how the suites are invoked.