Execution layer¶
How TrackGen emits device work: stream resolution, the execution policy shared by every
producer (record into your capture, eager, or replay of the producer’s own CUDA
graph; segment by segment for a pipeline with HOST stages), the device-side
conditional node skip, bounded loops, the host operations of HOST stages and
the capture errors; then the per-env seed and epoch state, and the torch
interop. Calling a generator and Rules for recording into your graph show them in use;
CUDA Graph — capture, replay, and buffer reuse explains the mechanics.
Launch contexts and runners¶
Execution layer: how track_gen emits device work.
LaunchContext pins a call to one device and one stream so the work can be recorded
into a caller’s CUDA graph (Warp ScopedCapture, capture_begin(external=True), or
torch.cuda.graph) and replayed correctly. GraphRunner is the execution policy
of one producer (record into the caller’s capture / eager / replay of the producer’s
own CUDA graph, its self-graph, a
pipeline with HOST stages segment by segment). HostOps (ctx.host()) is how a
HOST stage reaches the host. unconditional() keeps CUDA conditional nodes out of the
captures recorded in its block. RebindAfterCaptureWarning flags a bound input replaced
after a capture recorded the method that reads it.
- class LaunchContext[source]¶
Bases:
objectOne device + one stream; the only way library code emits device work.
Construct with
for_call()at a public API boundary and pass it down (ctx=) to every helper. Helpers acceptctx=Noneand resolve it withresolve(), which prefers the ambient context.Besides launches, copies and fills it offers device-side control flow that stays recordable into any capture (
skip_unless(),bounded_loop()) and, inside a HOST stage only, host operations on its stream (host()).- device¶
- stream¶
- classmethod for_call(device, stream=None, *, default=None)[source]¶
Context for a public call on
device.With no explicit
stream, an ambient context on the same device (set by an enclosingscope()) is reused; otherwise the stream is resolved byresolve_stream().- Return type:
- classmethod current(device)[source]¶
Context pinned to Warp’s CURRENT stream on
device(no adoption).For code that must stay on the stream Warp allocates and zero-fills on (e.g.
PerEnvSeededRNGconstruction, the facades’ backward-compatible_run()), regardless of ambient or torch state.- Return type:
- property capturing: bool¶
Whether this context’s stream is capturing. Informational only: enqueue code must not branch on it.
- launch(kernel, dim, inputs=(), outputs=(), *, block_dim=256, max_blocks=0)[source]¶
wp.launch(kernel, dim, inputs, outputs)on this context’s device and stream (block_dimandmax_blocksas inwp.launch).
- copy(dst, src, *, dst_offset=0, src_offset=0, count=0)[source]¶
wp.copy(dst, src)on this context’s stream:countelements (0: the whole ofsrc) fromsrc[src_offset:]intodst[dst_offset:].
- fill(arr, value)[source]¶
Fill
arrwithvalueon this stream (neverarr.zero_()/fill_(), whose memset targets Warp’s current stream).- Parameters:
arr (array)
- Return type:
None
- skip_unless(flag, body)[source]¶
Run
body(self), skipped on device whenflag[0] == 0where that is possible; otherwise run it unconditionally. ReturnsTruewhenbodywas recorded under a conditional node,Falsewhen it ran (or was recorded) unconditionally.CONTRACT:
bodymust be an exact no-op whenflag[0] == 0(it must leave every buffer bitwise unchanged), because whether the skip happens depends on the capture, not on the data:inside a capture that Warp registered on this context’s stream (Warp
ScopedCapture,capture_begin(external=True), ortrack_gen.interop.torch.registered(conditional=True)undertorch.cuda.graph) on a device with conditional-node support (CUDA Toolkit and driver >= 12.4):bodyis recorded under a CUDA conditional node that readsflag[0]at replay time (wp.capture_if);anywhere else (eager calls, the CPU device, an unregistered
torch.cuda.graphcapture or one registered withoutconditional=True, an unmarked Warp registration whose stream is torch’s current, capturing stream in this thread (a hand-madecapture_begin(external=True)of torch’s capture, or your ownScopedCaptureof torch’s current stream unless insideconditional()), a stream that only joined a capture registered on another stream, an older driver, insideunconditional()):bodyruns (or is recorded) unconditionally.
A graph holding a conditional node cannot be used as a CUDA child graph (e.g.
wp.capture_if(c, on_true=that_graph)).wp.capture_ifalso ends and restarts the stream’s capture around the node, which CUDA refuses while a side stream that joined the capture has not been joined back (an unjoined fork invalidates the capture), and which gives the capture a new CUDA capture id: an external capturer that tracks its capture by id (torch’s caching allocator undertorch.cuda.graph) loses track of it for the rest of the capture. Hence the opt-in under torch, andunconditional()to opt out of a Warp-registered capture.Never takes Warp’s eager
capture_ifpath, which reads the flag back on the host and synchronizes the stream.- Parameters:
flag (array) –
[1]int32array on this context’s device (e.g. written bytrack_gen.ops.any_set()earlier on the same stream).body (Callable[[LaunchContext], None]) –
body(ctx)emitting its work throughctx(this context).
- Raises:
TypeError –
flagis not a[1]int32wp.array.ValueError –
flagis not on this context’s device (wp.capture_ifwould miss the capture and read the flag on the host).
- Return type:
- bounded_loop(n_iters, body, *, active=None, first_check=0, check_every=1)[source]¶
Run
body(ctx, it)forit in range(n_iters)(ita static Python int), stopping early onceactive[0] == 0where the capture state allows it.Iterations
[0, min(first_check, n_iters))always run. Then come the windows[lo, min(lo + check_every, n_iters))forlo = first_check, first_check + check_every, ...: each is preceded by a check ofactive[0]and may be skipped, with every later one, once that check reads 0. Withoutactiveevery iteration runs (a plain unrolled loop).How a window check is lowered depends on where the call runs, never on data:
(a) a host poll (one 4-byte read) that stops the loop: the CPU device, eager, with no CPU graph capture open on it;
(b) a CUDA conditional node per window (
wp.capture_if, viaskip_unless()): inside a Warp-registered capture of this context’s stream with conditional-node support (CUDA >= 12.4), e.g. a producer’s own graph, a WarpScopedCaptureortrack_gen.interop.torch.registered( conditional=True);(c) every window runs (or is recorded) unconditionally: eager calls on CUDA (they never host-sync), plain
torch.cuda.graphcaptures, older drivers, CPU graph captures, recordings insideunconditional().
CONTRACT (checked by the conformance kit,
bounded.absorbing):activeis a[1]int32 array on this context’s device that the stage writes on the device before the first check (itsenqueue, before the loop, whenfirst_check == 0; otherwise usually the iterationlo - 1that precedes each window), in every generation; once a check readsactive[0] == 0, every remaining iteration leaves everything read after the loop bitwise unchanged (an ABSORBING freeze); the body emits only through the context it is given and mutates no Python state read after the loop. The three lowerings then give bitwise identical results; how many iterations actually execute is unspecified (n_itersis a bound). A stage whose loop passesactivedeclarescapture = CaptureClass.BOUNDED.Under lowering (b) each window is the body graph of a CUDA conditional node, which may hold kernel launches, memsets and device-to-device copies only: a window may launch kernels and
ctx.copy/ctx.fillbetween device arrays onctx’s stream, and nothing else (no copy to or from pageable host memory, no event, no other stream; a copy from pinned, device-mapped host memory is accepted by CUDA but keep windows to device memory for portability). Anything else fails insidewp.capture_ifwhile the caller’s capture is being recorded.check_stage(..., devices=["cuda:0"])(bounded.lowering) records every window that way.- Parameters:
n_iters (int) – The iteration bound (
>= 0).body (Callable[[LaunchContext, int], None]) –
body(ctx, it), emitting its work throughctx.active (array | None) –
[1]int32 flag on this context’s device (None: no checks).first_check (int) – The first iteration that may be skipped (
>= 0).check_every (int) – Iterations per window (
>= 1).
- Raises:
TypeError – a non-int count (
boolincluded), oractivenot a[1]int32wp.array.ValueError – a negative count,
check_every < 1,activeon another device, oractivepassed inside a stage that declarescapture = CaptureClass.STATIC.
- Return type:
None
- host()[source]¶
The
HostOpsof the HOST stage running on this context: readbacks, uploads and waits on this context’s stream through the stage’s build-time pinned host buffers.- Raises:
CaptureError – not inside the
enqueueof a pipeline stage on this device.ValueError – inside the
enqueueof a stage that does not declarecapture = CaptureClass.HOST(the error a wrong declared capture class gives, as forbounded_loop(active=...)in a STATIC stage andStageBuilder.host_buffer), or of a legacy generator spec.NotCapturableError – this context’s stream is capturing, or a CPU graph capture is open on the device (host work would be recorded, or read stale data).
- Return type:
- class GraphRunner[source]¶
Bases:
objectExecution policy (record / eager / self-graph) for one producer.
- Parameters:
device – the producer’s Warp device.
capturable – whether the enqueue body obeys the graph-capture contract.
Falseruns eagerly outside a capture and raisesNotCapturableError(before any launch) inside one.legacy_stream – the body contains a legacy-ABI plugin (no
accepts_ctx) whose own launches go to Warp’s CURRENT stream (see the module docstring for what that changes). Recording into a caller’s capture requires the capture stream to be that current stream (LegacyPluginStreamErrorotherwise).default_stream – stream used when a call passes none and no ambient context applies (see
resolve_stream()).owner – object anchored on a caller’s Warp graph when the body is recorded into it, so the buffers the graph references outlive the producer. Held weakly by the runner.
name – label used in error messages.
A body passed to
run()may also be a segmented body (one part perProgramsegment, built by the Program): see the SEGMENTED row of the module docstring.A runner is single-writer: running the same producer concurrently from two threads is a race on its output buffers (use one producer per concurrent caller). Different producers are independent: each self-graph is captured on the producer’s own private stream, so no process-wide lock is taken (legacy-plugin producers, and calls on Warp’s current stream while one of them may still self-capture, excepted).
- __init__(device, *, capturable, legacy_stream=False, default_stream=None, owner=None, name='', _host_ids=(), _legacy_capture=None)[source]¶
- property graph: Graph | None¶
The default-key self-graph, once captured (first CUDA call with
key=Noneoutside a capture);Noneon CPU, for a non-capturable producer (also a segmented one: its parts have one graph each), or before that call.
- graph_for(key=None)[source]¶
The whole-body self-graph cached for
key(seerun()), if captured;Nonefor the key of a segmented body (seesegment_graphs_for()).- Return type:
Graph | None
- property graph_keys: tuple¶
The keys of every self-graph captured so far (
None= default), segmented bodies included (their graphs:segment_graphs_for()).
- segment_graphs_for(key=None)[source]¶
The per-part self-graphs of the segmented body cached for
key, one per part in order (Noneat a HOST part), orNonebefore they were captured (and for a body that is not segmented).- Return type:
tuple | None
- property device¶
The producer’s Warp device (a
warp.context.Device).
- property capturable: bool¶
Whether the body may be recorded into a caller’s capture (the constructor’s
capturable);Falseruns eagerly and raisesNotCapturableErrorinside a capture.
- property legacy_stream: bool¶
Whether the body holds a legacy-ABI plugin that launches on Warp’s current stream (the constructor’s
legacy_stream).
- property default_stream: Stream | None¶
The stream used when a call passes none and no ambient context applies (
Noneon CPU, or when the constructor got none).
- context(stream=None)[source]¶
The launch context of one call (explicit
stream> ambient >default_stream> torch capture stream > Warp’s current stream).- Return type:
- ensure_not_capturing()[source]¶
ensure_not_capturing()for this runner’s device and default stream: call it before the producer’s first allocation.- Return type:
- serial(ctx)[source]¶
The lock a call on
ctxmust hold while it decides its mode and runs (a context manager; a no-op for most calls).runtime._CAPTURE_LOCK(reentrant) for a producer that self-captures legacy-plugin code on Warp’s current stream (CUDA), and for any call resolving to that stream (or of a legacy producer) while a legacy producer on the device may still self-capture there. Callers that issue their own launches ahead ofrun()(e.g. a reseed) hold it around both. An eager run of legacy code holds the lock on its own (see the module docstring).- Parameters:
ctx (LaunchContext)
- prepare(enqueue)[source]¶
Run
enqueueonce, for real, outside any capture, and block until it finished.Launching for real loads every module / kernel overload / block-dim variant the hot path uses (
force_module_loadandload_modulemiss launch-time overloads), so later recordings never load modules while capturing. The pass leaves a valid result in the producer’s buffers.When the resolved stream is not Warp’s current stream (a
default_streamor an ambient scope), it first waits on the current stream, where the producer’s buffers were just allocated and zeroed. The host then blocks on the resolved stream (a cold, construction-time cost), so the result is complete for any later stream or readback.Every
LaunchContext.bounded_loopof the pass runs all its windows (forced unrolled lowering), so every kernel a window can emit is loaded.- Raises:
CaptureError – if the resolved stream or Warp’s current stream is capturing, or torch is capturing in the calling thread.
- Return type:
None
- run(enqueue, *, stream=None, policy='auto', sync=False, key=None)[source]¶
Run
enqueueonce under the execution policy; returns the call’s resolvedLaunchContext.A non-capturable runner refuses a caller’s capture (RECORD) and a CPU graph capture (
wp.ScopedCapture("cpu")) withNotCapturableError, before any launch (seecheck_record()). Outside a capture it runs a plain body eagerly; a segmented body (one part perProgramsegment) runs its HOST parts eagerly and replays the other parts’ self-graphs on CUDA (SEGMENTED, see the module docstring).- Parameters:
enqueue – the producer’s body,
enqueue(ctx) -> None(or a segmented body built by aProgram).stream – optional
wp.Stream(or"torch") for this call.policy (str) –
"auto"(record / self-graph / eager as above) or"eager"(record when capturing, otherwise run the body directly; never self-graph).sync (bool) – block the host on the resolved stream afterwards; skipped when recording, and while torch is capturing in the calling thread.
key – hashable self-graph cache key identifying WHICH body
enqueueis (e.g.(advance, mask.ptr)). The self-graph of a key is captured on its first SELF-GRAPH call and replayed by every later call with an equal key, so equal keys must mean identical bodies (same launches, same buffer pointers).None(default) is the producer’s default body (graph). A non-Nonekey requiresprepare()to have run (a keyed body may not be idempotent, so it is never used as a warm-up pass). Ignored when recording or eager. A legacy-plugin producer runs non-Nonekeys eagerly (it self-captures on Warp’s shared current stream, once). At mostMAX_KEYED_GRAPHS(16) non-Nonekeys get a self-graph; calls with a further new key run eagerly (with a one-timeRuntimeWarning).
- Return type:
- check_record(ctx)[source]¶
Raise where
run()would refusectxbefore any launch:ctxis capturing and this producer cannot be recorded there, or a CPU graph capture (wp.ScopedCapture("cpu")) is open on the device and the producer is not capturable. A no-op otherwise. Lets a caller validate before issuing its own launches ahead ofrun()(e.g. a reseed).- Raises:
NotCapturableError – the producer is not capturable (it names its HOST stages).
LegacyPluginStreamError – a legacy-plugin producer and the capture stream is not Warp’s current stream.
- Parameters:
ctx (LaunchContext)
- Return type:
None
- class HostOps[source]¶
Bases:
objectHost operations of a HOST stage, bound to one call’s stream (
ctx.host()).Every copy runs on
ctx.streamthrough a pinned host buffer the stage allocated at build (StageBuilder.host_buffer), and every wait blocks on that stream only, so a HOST stage never allocates per call and never waits on the whole device. On the CPU device copies are synchronous andsync()does nothing (the same code path runs).Obtain it with
h = ctx.host()inside theenqueueof a stage declaringcapture = CaptureClass.HOST:def build(self, b, p): seeds_h = b.host_buffer(wp.int32, b.E) xy_h = b.host_buffer(wp.vec2f, b.E * p.points) xy = b.write("xy", wp.vec2f, per="raw") def enqueue(ctx): h = ctx.host() seeds = h.readback(b.seed, via=seeds_h) # numpy view of seeds_h pts = my_host_algorithm(seeds) # [E * points, 2] float32 h.upload(xy, pts, via=xy_h) return enqueue
- sync()[source]¶
Block the host until the work queued on
ctx.streamfinished (CPU: no-op). A stream synchronize in relaxed capture mode, never a device-wide one.- Return type:
None
- read_i32(arr, index=0)[source]¶
arr[index]of an int32 device array, through the slot’s build-time pinned[1]buffer (copy onctx.stream, thensync()).- Raises:
TypeError –
arris not an int32wp.array, orindexnot an int.ValueError –
arron another device, not contiguous, orindexout of range.
- Parameters:
arr (array)
index (int)
- Return type:
- readback(src, *, via)[source]¶
Copy
src(a device array) into the host bufferviaonctx.stream, wait, and return a numpy VIEW ofviashaped likesrc(vector dtypes add a trailing component axis). The view is valid untilviais used again.- Raises:
TypeError – a non-array, or
viaof another dtype.ValueError –
srcon another device or not contiguous,vianot a host buffer (not pinned, on CUDA) or too small.
- Parameters:
src (array)
via (array)
- Return type:
- upload(dst, data, *, via)[source]¶
Write
data(anything numpy converts tovia’s dtype,dst.sizeelements: e.g. an[n, 2]float array for avec2fdst) into the host buffervia, copy it to the device arraydstonctx.streamand wait, soviais reusable on return.- Raises:
TypeError – a non-array, or
viaof another dtype.ValueError –
dston another device or not contiguous,vianot a host buffer (not pinned, on CUDA) or too small,dataof the wrong size.
- Parameters:
dst (array)
via (array)
- Return type:
None
- resolve_stream(device, stream=None, default=None)[source]¶
Resolve the single stream a public call runs on (see module docstring).
- Parameters:
device – a Warp device or device string.
stream –
None, awp.Stream, or"torch".default – stream to use when
streamisNoneand no ambient context applies (e.g. a stream bound at construction).
- Return type:
Stream | None
- resolve(ctx, device)[source]¶
ctxif given, elseLaunchContext.for_call()ondevice.- Parameters:
ctx (LaunchContext | None)
- Return type:
- is_capturing(stream)[source]¶
True when
streamis capturing (any capture: Warp, torch, external).- Parameters:
stream (Stream | None)
- Return type:
- unconditional(enabled=True)[source]¶
Record no CUDA conditional node in this block (thread- and task-local).
Inside the block
LaunchContext.skip_unless()records its body unconditionally in every capture, soCourse.regenerate(mask=...)and everyLaunchContext.bounded_loop()window (therepulsivegenerator, a plugin’sBOUNDEDstage) take lowering (c) (every window recorded), with the same results. Use it around a recording into a Warp-registered capture (wp.ScopedCapture,wp.capture_begin) that must not get conditional nodes:a graph you will use as a CUDA child graph (
wp.capture_if(c, on_true=graph), a graph node of another graph): a graph holding a conditional node cannot be one;a capture whose stream has an outstanding fork (a side stream that joined the capture and has not been joined back):
wp.capture_ifends and restarts the stream’s capture around each node, which CUDA refuses with an unjoined fork (cudaErrorStreamCaptureUnjoined), invalidating your capture.
A producer’s own graph captured in the block (its first CUDA call outside a capture, e.g.
gen.graph) is recorded unconditionally too, and stays so. Eager calls are unchanged.unconditional(False)restores the default inside an enclosing block.
- conditional(enabled=True)[source]¶
Let your own Warp capture of torch’s current stream record CUDA conditional nodes (thread- and task-local).
LaunchContext.skip_unless()(a maskedCourse.regenerate, everyLaunchContext.bounded_loop()window) records no conditional node in a Warp registration nobody opted in whose stream is torch’s current stream while that stream is capturing: Warp does not record who began the capture, and a hand-madecapture_begin(external=True)of a runningtorch.cuda.graphmust not get one (wp.capture_ifrenews the capture id torch’s caching allocator tracks). That also catches YOUR OWNwp.ScopedCaptureof torch’s current stream (e.g. undertorch.cuda.stream(t)+wp.ScopedStream(wp.stream_from_torch(t))), whose windows are then recorded unconditionally: same bytes, no device-side skip. Inside this block such a capture records conditional nodes; use it only when torch is NOT the capturer (undertorch.cuda.graphusetrack_gen.interop.torch.registered(conditional=True)instead).unconditional()wins over it; the library’s own graphs (a producer’sgraph) are unaffected (Warp captures them, marked).conditional(False)restores the default inside an enclosing block.
Capture errors¶
Their causes and fixes are summarized in Errors and warnings.
- exception CaptureError[source]¶
Bases:
RuntimeErrorA call was made in a capture state it does not support.
For example, constructing a generator (which runs one real warm-up pass) while the resolved stream is being captured into a CUDA graph.
- exception NotCapturableError[source]¶
Bases:
CaptureErrorA non-capturable producer was asked to record into a caller’s capture (a CUDA graph capture, or a CPU graph capture:
wp.ScopedCapture("cpu")).Raised BEFORE any device work is issued, so the caller’s capture is left untouched. Outside a capture the same producer runs: its HOST stages eagerly, the rest of a pipeline from its own graphs (segmented execution, see
GraphRunner).- name¶
The producer’s name (e.g. the generator name).
- stage_ids¶
The ids of the HOST stages that make it non-capturable (
()when unknown, e.g. a bareGraphRunner(capturable=False)).
- exception LegacyPluginStreamError[source]¶
Bases:
CaptureErrorA legacy-ABI plugin would launch on the wrong stream under capture.
Legacy generator plugins (registered without
accepts_ctx=True) issue their ownwp.launchcalls on Warp’s CURRENT stream. Recording one into a caller’s capture is therefore only correct when the capture stream IS Warp’s current stream.
- exception RebindAfterCaptureWarning[source]¶
Bases:
UserWarningA bound input array was replaced after a hot method that reads it was recorded into a capture.
The recorded graph keeps reading the arrays that were bound when it was recorded, so after a rebind its replays silently read the old buffers. Emitted (once per bind call) by
Course.bind,Component.bindand the utilities’bind/bind_inputs/bind_windowwhen a pointer changes after theirquery()/update()ran while its stream (or a CPU graph capture) was capturing; rebinding the same arrays is silent. The documented recipe “rebind, then record again” also warns once: filter this category when you re-record on purpose:import warnings import track_gen.exec warnings.simplefilter("ignore", track_gen.exec.RebindAfterCaptureWarning)
Seeds and epochs¶
Per-env seeds and device-side epochs, the pair that keys every generation; see Seeds, epochs and the generation counter.
Per-env seeds and epochs: the device-side state that keys every generation.
A SeedState holds two [E] int32 arrays:
seeds: the per-env base seeds. Either an array the state OWNS (built fromNone/ an int, or copied withcopy=True), or a caller array BOUND by pointer (awp.array, or aPerEnvSeededRNG’sseeds_warp).SeedStatenever writes bound seeds, and neither do the generators: write new seeds into that array yourself (they are read at every generation, also by every graph replay), or use an owned state andSeedState.set(). The rng itself does write itsseeds_warp:PerEnvSeededRNG.set_seeds_warpandCourse.generate(seeds=...)overwrite it in place, and it IS the caller’s array when the rng (orCourseConfig.seeds) was given awp.array.epoch: owned, zero-initialised, and only ever changed on device (SeedState.advance(),SeedState.reset_epoch(),SeedState.set()), so it can be driven from inside a CUDA graph.
Every generation first folds the two into an EFFECTIVE seed per env
(_effective_seed_k, the first launch of every generator body):
effective = seed if epoch == 0
= hash32(u32(seed) ^ hash32(u32(epoch) * 0x9E3779B9)) & 0x7FFFFFFF
otherwise
Epoch 0 leaves the seed unchanged, so a never-advanced env generates exactly what its
base seed alone gives. A MASKED generation
(effective(out, mask=m)) re-derives the effective seed of the envs with
m[e] != 0 only: every other env keeps the effective seed its current output
was built from, so it is recomputed to identical values whatever happened to its
(seed, epoch) since. Determinism: the pair (seed, epoch) fully reproduces
an env. A generator whose seed for that env is
effective_seed(seed, epoch) at epoch 0 produces the identical track or gate
sequence (effective_seed() is the bit-exact host twin of the kernel).
- class SeedState[source]¶
Bases:
objectPer-env base seeds plus a device-side epoch counter (see the module docstring).
- Parameters:
num_envs – number of environments
E(>= 1).device – Warp device of the state.
seeds –
None(owned0 + arange(E)), an intk(ownedk + arange(E)with int32 wrap-around, the same expansion asPerEnvSeededRNG(seeds=k)), an[E]int32wp.arrayondevice(BOUND by pointer, never written bySeedState), or aPerEnvSeededRNG(binds itsseeds_warpby pointer, sorng.set_seeds_warp(...)keeps reseeding generation). The rng must therefore live ondevice.copy – with an array or rng
seeds, take an OWNED copy instead of binding (thenset()is allowed).default_stream – optional
wp.Streamthe methods use when a call passes neitherstream=norctx=(and no ambient context applies), astrack_gen.exec.resolve_stream()’sdefault. The facades pass their constructorstream=, sogen.seeds.advance()is ordered withgen.generate().
Construction allocates (and, for an owned int expansion, uploads) on Warp’s current stream: build it outside any CUDA graph capture. Every other method is one or two launches on the call’s stream: allocation-free, sync-free and capturable.
- Raises:
TypeError –
seedsis not one of the accepted kinds.ValueError –
num_envs < 1, or a seed array that is not[E]int32 ondevice.
- property epoch: array¶
[E]int32 per-env epoch (owned; changed only on device).
- property bound: bool¶
Whether
seedsis a caller array bound by pointer (never written bySeedState;set()raises).
- check_mask(mask, name='mask')[source]¶
Validate a contiguous
[E]int32 mask on this state’s device (host-side). Contiguous, because the producers cache one CUDA graph of their own per mask POINTER: two views with one pointer and different strides would share it.- Parameters:
name (str)
- Return type:
None
- advance(mask=None, *, stream=None, ctx=None)[source]¶
epoch[e] += 1for every env (mask=None) or wheremask[e] != 0.One launch on the call’s stream; capturable (inside a caller’s graph every replay advances again).
- Parameters:
mask (array | None) – optional
[E]int32 mask on this device, read on device.stream – optional
wp.Stream(or"torch"); resolved pertrack_gen.exec.ctx – a
LaunchContextto use instead ofstream.
- Return type:
None
- reset_epoch(mask=None, *, stream=None, ctx=None)[source]¶
epoch[e] = 0for every env (mask=None) or wheremask[e] != 0(one launch; capturable).- Parameters:
mask (array | None)
- Return type:
None
- set(seeds, ids=None, *, stream=None, ctx=None)[source]¶
Write new base seeds into an OWNED state and zero those envs’ epochs.
ids=None:seedsis[E]int32 and replaces every env’s seed. Otherwiseidsis[n]int32 env indices andseedsthe[n]int32 new seeds (seeds[i]goes to envids[i]; duplicate ids race). One launch (or a copy plus a launch); capturable.- Raises:
RuntimeError – the seeds are
boundto a caller array (SeedStatenever writes bound seeds): write that array yourself, or build the state withcopy=True.ValueError – wrong shape, dtype or device.
- Parameters:
seeds (array)
ids (array | None)
- Return type:
None
- effective(out, mask=None, *, stream=None, ctx=None)[source]¶
Write the effective per-env seeds (see the module docstring) into the
[E]int32out: for every env, or (mask) only wheremask[e] != 0, leaving the other entries ofoutuntouched. One launch; capturable. This is the first launch of every generator body (after the epoch advance, if any).- Parameters:
out (array)
mask (array | None)
- Return type:
None
- effective_seed(seed, epoch)[source]¶
Host twin of the effective-seed kernel: the seed env generation actually uses for base seed
seedat epochepoch(both taken as int32).effective_seed(s, 0) == s; forepoch != 0the result is a non-negative int32 hash of(s, epoch). A generator whose rng seed for an env iseffective_seed(s, k)(at epoch 0) reproduces that env’s batch at(s, k)exactly.
Torch interop¶
Registering a torch.cuda.graph capture with Warp, and tying producers’ lifetime to a
torch graph; see CUDA graphs in a sim loop. Importing track_gen does not import
torch.
PyTorch interop for recording track_gen work into torch.cuda.graph captures.
A plain torch.cuda.graph(g) capture is invisible to Warp: library calls made
inside it are still recorded into g (they adopt torch’s capture stream, see
track_gen.exec), but two Warp features need the capture to be REGISTERED
with Warp:
lifetime anchoring: a producer recorded into a registered capture is attached to the Warp graph object of that capture;
device-side conditional skips (
track_gen.exec.LaunchContext.skip_unless(), used byCourse.regenerate(mask=...)and by every window of an iterativeBOUNDEDstage such as therepulsivegenerator), opt-in withconditional=True: otherwise the guarded body is recorded unconditionally (always correct, never skipped). A hand-made Warp registration of torch’s capture (wp.capture_begin(stream=..., external=True)on torch’s capture stream) gets no conditional node either: opt in throughregistered(). (Your ownwp.ScopedCaptureof torch’s current stream, when torch is not capturing, opts in withtrack_gen.exec.conditional().)
registered() registers torch’s capture with Warp for the duration of a
with block; retain() ties Python objects to the torch graph’s lifetime (here
course is a bound Course and done your [E] int32 episode-end mask):
import torch
import track_gen.interop.torch
g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
with track_gen.interop.torch.registered(graph=g):
course.step()
course.regenerate(mask=done, advance=True)
track_gen.interop.torch.retain(g, course) # keep the course's buffers alive with g
This module imports torch lazily (only when a function is called).
- registered(stream=None, *, graph=None, capture_mode=None, conditional=False)[source]¶
Register an open
torch.cuda.graphcapture with Warp for this block.Use INSIDE
torch.cuda.graph(g). Resolves the capture stream to the library’s cached Warp wrapper of that handle (never a throwaway one: dropping awp.stream_from_torchwrapper unregisters the handle process-wide), then:if Warp already has a capture registered on that handle (e.g. an enclosing
registered()or WarpScopedCapture), yields that registered stream and does nothing else (graph=andconditional=are then ignored: the enclosing registration owns the Warp-side graph object);if the stream only JOINED a capture Warp registered on another stream (a side stream forked from it), yields the stream and registers nothing (a second registration of the same capture would break the first one’s teardown); the conditional skip then does not apply on this stream;
otherwise opens
wp.ScopedCapture(stream=s, external=True)on it (which does not begin a new CUDA capture, only registers torch’s) and closes it with the SAME stream object at the end of the block (Warp looks an open capture up by stream object identity).
Inside the block, library calls adopt the stream as usual and additionally get lifetime anchoring and, with
conditional=True, conditional skips (LaunchContext.skip_unless, CUDA 12.4+). The registration ends with the block; the torch capture continues.conditional=Truecaveat: a conditional node (wp.capture_if) ends and restarts the CUDA capture of the stream, which gives it a new capture id, and torch’s caching allocator recognises its graph’s capture by the id taken atcapture_begin. After the first conditional node, torch therefore no longer routes the allocations of later torch ops in the sametorch.cuda.graphto the graph’s private pool (they either fail to capture or take blocks from the general pool that eager code may reuse while the graph still writes them), and other torch features that look the capture up by its id may fail. The first conditional node comes with a maskedCourse.regenerate(mask=...)or with ANY generation that runs aBOUNDEDstage (therepulsivegenerator, a plugin’sctx.bounded_loopwithactive=: one conditional node per window), not only with masked regeneration. So withconditional=True, record those calls AFTER every torch op of the graph that allocates (or preallocate their outputs and useout=/ in-place ops), up to the end of thetorch.cuda.graphblock, not just of this one. Ending the capture also fails while a side stream that joined it has not been joined back. The graph also cannot be used as a CUDA child graph. Withoutconditional=Truethe masked regeneration and everyBOUNDEDwindow are recorded unconditionally (exact no-ops when skipped would be, costing their full work per replay) and torch’s capture is never touched.- Parameters:
stream –
None(torch’s current stream on the current CUDA device, which is the capture stream insidetorch.cuda.graph), atorch.cuda.Stream, or awp.Stream.graph – optional
torch.cuda.CUDAGraphbeing captured. When given, the Warp-side graph object of the registration (it holds the producers anchored during the block and the Warp modules the recorded kernels use) isretain()-ed on it, so they live as long asgraph. Without it they are released when the block exits: keep the producers alive yourself, e.g.retain(g, course).capture_mode – the mode torch began the capture with (
torch.cuda.graph(..., capture_error_mode=...)):"global"(default, torch’s default),"thread_local","relaxed", or awp.CaptureMode.conditional (bool) – allow
LaunchContext.skip_unlessto record CUDA conditional nodes into this registration (a maskedCourse.regenerate, every window of aBOUNDEDstage; defaultFalse; see the caveat above).
- Yields:
The registered
wp.Stream(pass it asstream=if you like; library calls inside the block resolve to it anyway).- Raises:
track_gen.exec.CaptureError – the stream is not capturing (not inside
torch.cuda.graph).- Return type:
Iterator[Stream]
- retain(torch_graph, *owners)[source]¶
Keep
ownersalive for as long astorch_graphis.A CUDA graph stores raw device pointers; it does not keep the buffers it reads and writes alive. Replaying
torch_graphafter a producer (Course,TrackGenerator, …) recorded into it was garbage-collected reads freed memory.retain(g, course)stores strong references in an attribute of the torch graph object (g._track_gen_owners), so they are released together withg. Idempotent per object;Noneowners are ignored.- Raises:
TypeError –
torch_graphcannot carry attributes (no__dict__).- Return type:
None