Running until every sample is done#
A step kernel can finish its own samples: it assigns its terminated mask
(terminated = t >= t_final) and counts each newly finished sample into
one counter plane, finished_count (eagle.FINISHED_PLANE). Its
sidecar then carries a finish declaration
(eagle.sidecar.read_finish()), and eagle.run_until_done() runs the
whole batch:
report = eagle.run_until_done(plan, max_steps=10_000,
x=x, v=v, k=k, omega=omega, dt=1e-3)
assert report.done # report.launches, .compactions, .reorders, ...
The call binds the plan once (the caller passes every plane but the counter,
the active-set planes and, optionally, the mask, which the runner allocates)
and builds the loop above: a eagle.repeat_while() guarded by
finished_count != n, and, for an artifact built with
Guard(active_set=True), the eagle.compaction_body() cadence over an
eagle.ActiveSet, with reorder=theta for the physical reorder.
Whether it compacts is read off the artifact, never a flag. On the device the
loop is one CUDA graph (one WHILE node that reads the counter on the device),
captured on the first run and replayed after; a reordering run ends with the
planes back in sample order.
eagle.until_done() returns the eagle.Runner instead:
runner.run() runs (a second run continues from the current mask, so it
launches nothing once everything is done), runner.reset() clears the mask
and the counter, and runner.loop is the eagle.RepeatWhile to
compose into a larger eagle.GraphPipeline. The explicit path stays
public; the runner is written in it.
Letting the run pick the steps per launch#
A kernel built with @hawk.kernel(steps="auto") (or
hawk.steps(kernel, "auto")) takes up to 64 steps per launch and reads how
many from a one-cell device word, eagle.FUSED_STEPS_PLANE, that the
runner owns. The same call runs it; nothing else changes for the caller:
report = eagle.run_until_done(plan, max_steps=10_000, x=x, v=v, dt=1e-3)
report.launches_by_k # e.g. {16: 1, 32: 1, 64: 15}
assert report.steps <= 10_000 # the steps the loop ran, never above max_steps
After each launch one single-thread policy kernel picks the next launch’s
steps: the power of two nearest sqrt(2R / h) (eagle._core.band_k()),
held between AUTO_K_MIN (8) and the artifact’s steps_max, where R
is a compaction’s cost in steps of a full-live launch and h the rate at
which samples finish; a launch in which nothing finished is followed by the
longest launch allowed. The first launch takes AUTO_K0 (16) steps. On a
batch whose samples all run about as long it climbs to steps_max steps per
launch, the same fusion a hand-written multi-step kernel gets; on a batch
whose stop steps are spread out it stays shorter, and an artifact built with
Guard(active_set=True) compacts between the launches of the band, so the
live samples keep sharing warps (a batch that fits inside one resident wave
skips the compaction and runs every launch at steps_max). The last
launch takes exactly the steps left, so no sample takes more than
max_steps steps. eagle.RunReport.steps is the steps the loop ran:
a sample still running at the end took exactly that many, and the
longest-running sample of a finished batch may have stopped inside the last
launch (report.exact_steps is True when steps is exactly what that
sample attempted: the cap stopped the run, or the last launch took one step).
A launch whose word is 1 runs the kernel’s straight-line single step, so it
costs what the one-step kernel costs.
The cadence is the policy’s, so every= is refused for such a kernel;
reorder=theta still opts into the physical reorder (it is off by default,
because a fused launch reads each plane once per launch and gains less from
it). On the host a one-step launch runs the single step vectorised across
samples while a fused launch runs each sample’s scalar loop, so the host does not simply follow the band. runner.host_policy defaults
to "auto": "tiled" (steps_max steps per launch) when the entry’s
fused side is tiled, else "measured", which runs one step per launch and,
each time the live samples have halved, times one fused launch of 8 steps;
once that beats the one-step launch per step it continues under the band. A
batch whose samples all run to the end never probes and runs exactly the
one-step loop. runner.host_policy = "band" runs the device’s band on the
host (the same launches as the device). The results are those of one step per launch:
bit-equal on the host; on the device the fused loop may round a contracted
multiply-add differently from the single step, never from its own body run one
step at a time.
Termination is discrete: a vjp/jvp derived from a finishing kernel
does not finish samples (it zeroes terminated samples, as before). Launch the
derived kernels before the primal in a step, so they read the mask as it was
when the step began.
Fused or persistent, picked for you#
On the device a kernel can have two entries: the fused one (a launch per loop turn) and a persistent one (one launch whose lanes claim samples until none are left). The runner chooses between them, and the choice needs no flag:
a batch of at least
PICK_PERSIST_MULTIPLE(8) times the threads the persistent entry keeps resident across the device takes the persistent entry, and one of at most1 / PICK_FUSED_DIVISOR(a quarter) of what one fused wave holds takes the fused entry, from the first run and without timing the other;in between, run 1 takes the size pick and run 2 runs the other entry, so both have a measured wall;
later runs are settled: they carry no timing events.
The pick changes only when the other entry wins by more than 2 %
(eagle._until_done.PICK_HYSTERESIS is 0.98), so two near-equal entries
do not trade places. A settled runner times the other entry again only when a
run reads back something different from before: the longest sample’s steps
(to the nearest power of two below) or the step fill (to one decimal) moved
to another bucket. A runner that has timed both entries leaves its pick in a
process-wide cache keyed on the device, the kernel, the precision, the batch
size and that readback bucket, and a later runner with the same key takes it
untimed; nothing is written to disk.
eagle.RunReport.timed says whether a run was timed, and
report.probe whether it ran the entry the batch was not using, so a
caller that wants a clean measurement warms until a run is neither.
Persistent launches take chunk=, the steps a lane runs between two looks
for new samples (default 32, or whatever the launch policy chose for the
batch’s mean steps). eagle.Runner.launch_policy reports what was
chosen: block, grid, chunk, fill, rate and k.
eagle.RunReport.slot_fill is the steps run over the persistent
entry’s chunk slots, in (0, 1]; it is set only for a persistent run that
counted it, None otherwise. Pair the policy with
eagle.plan.BoundPlan.entry_reports(), whose entries carry registers,
local memory, text_bytes (code size) and demand (threads per SM the
launch asks for).