GeneratorSpec and the 0.1 plugin ABIs¶
Before you start
Who this page is for: you maintain a generator written as a
GeneratorSpec, a gate generator written as aGateGeneratorSpec, or a plugin that imports fromtrack_gen._src(the 0.1 plugin interface).What to do: a
GeneratorSpeckeeps working as it is; a new generator is better written as aStage(Writing a Generator, Writing a Gate Generator). Everything else on this page is deprecated: the status table says what warns and until when, and each section maps the old names to their replacement. The upgrade guide lists the steps for a 0.1 plugin: The _src registries, Plugins: built-in generators are Stages and Removed private names.How you test it:
check_generatorandcheck_gate_generatorcheck both forms against the same contract (Testing a Plugin).
Status¶
Interface |
Status |
Warning |
Runs in |
|---|---|---|---|
|
Supported. A |
none |
every 0.x release |
|
Supported; set |
none; recording it on another stream raises |
every 0.x release |
|
Deprecated: write an anchors |
|
0.x; removed in 1.0 |
The |
Deprecated: use |
silent ( |
0.x; removed in 1.0 |
The |
Deprecated: the pipeline’s order and normalize stages replace them. |
|
0.x; removed in 1.0 |
The |
Deprecated: use |
silent |
0.x; removed in 1.0 |
|
Deprecated: moved to |
|
0.x; removed in 1.0 |
|
Deprecated: the built-ins are Stage classes; read |
|
0.x; removed in 1.0 |
A |
Superseded by a |
none |
every 0.x release |
The hard rules, the registry and the conformance kit apply to both forms; what a
GeneratorSpec cannot do is listed where it matters (no dynamic field, no
per-point attributes, no provides, no per-env size).
The GeneratorSpec contract¶
The quick start of Writing a Generator shows a complete GeneratorSpec next to
the same generator written as a Stage.
What you implement¶
A GeneratorSpec has six fields:
Field |
Meaning |
|---|---|
|
The |
|
|
|
|
|
|
|
|
|
Optional: the generator’s |
alloc_scratch(config) -> scratch¶
Allocate your generator’s private working buffers once. Use fixed shapes derived
from config (num_envs, num_points, …), all on str(config.device).
Return any object exposing the buffers your generate function uses. Do not
allocate the output centerline or the validity array here: the pipeline owns those.
generate(seeds_wp, config, out_centerline, out_valid_wp, scratch, ctx=None) -> None¶
Write a closed centerline of config.num_points points per env into the output
arrays in place. Parameters:
Parameter |
Description |
|---|---|
|
|
|
The generator’s own copy of the configuration it was constructed with. With
|
|
|
|
|
|
The object your |
|
The call’s |
The rest of the generator contract is the Stage’s: the raw loop, its size and what you
do not have to guarantee (The Stage contract), and the
hard rules, with alloc_scratch in the place of the
StageBuilder and a constant salt of your own in the place of b.salt.
Streams: accepts_ctx¶
Set accepts_ctx=True and emit all device work through ctx: ctx.launch(kernel,
dim, inputs), ctx.copy(dst, src), ctx.fill(arr, value) (or
track_gen.ops.fill(ctx, arr, value)). Every track_gen.ops op takes ctx as
its first argument; pass ctx=ctx to any other helper that takes it. Never call
arr.zero_(), arr.fill_(), or a wp.launch / wp.copy without stream=:
they target Warp’s current stream, which need not be the stream the call resolved (a
side stream, or the stream torch is capturing). With accepts_ctx=True a generator can
be recorded into a caller’s CUDA graph on any stream.
A spec that leaves accepts_ctx=False is a legacy plugin. It still runs, inside
ctx.scope(), so the library helpers it calls inherit the stream, but its own direct
wp.launch calls go to Warp’s current stream. The generator’s own graph is then
recorded behind a process-wide lock, and recording it into a caller’s graph requires the
capture stream to be Warp’s current stream (LegacyPluginStreamError otherwise); see
Troubleshooting capture.
Opting out of capture: capturable=False¶
A generator may set capturable=False to opt out of hard rules 2 and 3: it may
allocate per call and use host-side control flow (Python loops driving stage
transitions, readbacks that branch on generated data). It is then a HOST generator:
TrackGenerator runs it eagerly on every call, on CUDA too, as the first segment
of the pipeline, and replays the rest of the pipeline from a graph
(HOST stages and segments). This is a supported path, not a fallback for a broken
generator. A caller cannot record it into a graph (NotCapturableError). Rules 1
(pure Warp, no torch) and 4 (fixed-shape buffers) still apply. GateGeneratorSpec
carries the same flag. A Stage declares capture = CaptureClass.HOST instead and
reaches the host through ctx.host() (Writing a host-driven (HOST) stage), which the conformance kit
can check; prefer it.
Before opting out, see how repulsive made a host-driven loop recordable: its
coarse-to-fine stage transitions, resample cadence and stall-check cadence form a fixed
schedule precomputed on the host from the configuration alone; the readback-driven early
exit became a per-env freeze on device (each env stops changing once it converges, and
every kernel skips a frozen env); and its growth loop is a ctx.bounded_loop over that
freeze (a BOUNDED Stage, Writing an iterative (BOUNDED) stage). Rule 5 still applies, with a caveat
for chaotic per-iteration methods: a generator that accumulates its CUDA gradients with
atomic_add can be only statistically reproducible on CUDA, since the float
summation order varies. repulsive avoids it with hand-written analytic adjoints (a
per-vertex gather, no atomics), so it stays byte-identical run to run on CPU and CUDA;
track_gen/_src/warp_generate_repulsive.py’s module docstring has the full account.
When your code runs¶
Your generate body runs once, eagerly, when the TrackGenerator is constructed
(the warm-up pass that loads every kernel). After that it runs once per eager call
(cpu) and once per recording: into the generator’s own graph on the first cuda
call, or into a caller’s graph when generate() is called inside the caller’s
capture. Replays do not run Python at all, so host scalars and Python state
freeze at record time. A capturable=False generator runs once per call instead (the
rest of the pipeline still replays from a graph), and raises NotCapturableError under
a caller’s capture. Masked regeneration and
Warm-up at construction describe the execution model.
Per-generator parameters¶
Declare your knobs as a Params class and pass it as
GeneratorSpec(..., params=AcmeParams). Users set them with
presets.track(generator="acme.wobbly", generator_params={...}) (or the same
argument of TrackGenConfig); alloc_scratch and generate read the resolved,
validated instance as config.generator_params (config.generator_params.lobes),
and describe() reports its schema. The class may have static fields only:
registry.add rejects a dynamic field, which needs a Stage’s b.param. The shared
fields (num_envs, num_points, scale, device, …) stay on the config.
Plugins written before Params existed read their knobs from a TrackGenConfig
subclass (getattr(config, "acme_lobes", 3)). That still works, but it is no longer
needed: move the knobs to a Params class and override from_legacy to read the old
fields, so the subclass configs your users already have resolve to the same values.
Registry-wide tools (benchmarks/compare_generators.py, viz/param_explorer.py)
build a plain TrackGenConfig for every registered generator, so a generator that
needs a config subclass must still fall back to defaults there; a Params class needs
nothing more.
Porting a GeneratorSpec to a Stage¶
The two quick starts of Writing a Generator are the same generator in both forms. The port is mechanical:
alloc_scratchbecomesbuild(self, b, params): each buffer becomes ab.scratch(name, dtype, shape), and the two outputsb.write("xy", ...)andb.write("valid", ...)(declared inwrites);generatebecomes theenqueue(ctx)closure thatbuildreturns:seeds_wpisb.seed,config.num_pointsisb.raw,config.num_envsisb.E,config.scaleisb.scale, and host scalars computed fromconfigare computed once inbuild;your constant salt becomes
b.salt("<stream>")(new streams, so new per-seed outputs; keep the constant to keep the bytes);params=becomes theParamsattribute, and a knob a curriculum varies can then become a dynamic field (b.param);capturable=Falsebecomescapture = CaptureClass.HOSTwithctx.host()for the readbacks (Writing a host-driven (HOST) stage).
The name, the entry point and the users’ generator= strings stay the same.
The legacy gate generator ABI: GateGeneratorSpec¶
Gate generators written against the 0.1 interface are GateGeneratorSpec objects
(exported by track_gen.plugin). They keep working, unchanged, through an adapter
stage (Legacy gate generators), and are deprecated: registering one is silent,
but the first GateGenerator, Course, build or conformance run that binds one
emits a DeprecationWarning (once per name), pointing at the code that constructed
the generator. They run until 1.0. A GateGeneratorSpec has the name,
alloc_scratch, capturable and accepts_ctx fields of a GeneratorSpec,
with the same meaning, the same hard rules and the same stream rules, plus:
Field |
Meaning |
|---|---|
|
|
|
|
|
|
|
Optional: a static |
presets.gates with a legacy generator refuses order=: choose its ordering with
GateGenConfig(gate_ordering=...).
To port one to an anchors Stage (Writing a Gate Generator):
move the body of
generateup to (excluding) its ordering intobuild/enqueue, writinganchors.xyat the generator’s own stride (per="anchors") instead ofout.position, andanchors.countinstead ofout.count;drop the ordering (
ops.angle_sortisorder.ccw) and the extent normalization (declarepreferred={"normalize": "extent"}), and declare the orders it accepts withexcluded_orderings(ororderings) andpreferred={"order": ...};turn
max_gates(config)intocapacity(params, layout) -> {"gates": A, "min_gates": k}, andalloc_scratchintob.write/b.scratchcalls.
The private warp_gate helpers that 0.1 gate plugins called from generate and
alloc_scratch (alloc_order_scratch, order_points, finish_ordered_gates
and the _GateStaging name) are deprecated and removed in 1.0: until then they keep
working, with a DeprecationWarning. When porting such a spec, drop the call: the
pipeline’s order.raw, order.ccw and order.random_pairs stages are the three
orderings order_points dispatched to (ops.angle_sort is order.ccw), and
preferred={"normalize": "extent"} replaces normalize_extent=.
Legacy _src helpers¶
Plugins written before track_gen.ops existed import private helpers from
track_gen._src.warp_pipeline (usually as _pipe), track_gen._src.warp_generate
and track_gen._src.warp_generate_hull. They are still importable: each keeps its
name and parameter list, which tests/test_legacy_plugin_abi.py pins for the 0.x
series, and delegates to the same kernels as the op that replaces it, so its results are
unchanged. They are deprecated in favour of track_gen.ops (they do not warn); port a
plugin with this table (Removed private names lists the private kernel
names that are gone):
Legacy helper |
Replacement |
|---|---|
|
Not needed: the facades initialise Warp before calling |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The helpers keep their own conventions: ctx=None as the last argument (the ambient
context when omitted), a debug-only ctx.sync() after their launches, and the config
objects the samplers read. track_gen._src.warp_pipeline.GenScratch moved to
track_gen._src.warp_generate.GenScratch; the old name still works and emits a
DeprecationWarning.
The built-in generators’ module functions keep their former signatures and scalar
kernels (their bytes are pinned by the fingerprint tests): generate_centerline_warp
and bezier_alloc_scratch, generate_hull_warp / hull_alloc_scratch,
generate_polar_warp / polar_alloc_scratch, generate_voronoi_warp /
voronoi_alloc_scratch, generate_checkpoint_warp / checkpoint_alloc_scratch,
warp_zprofile.apply_z_profile / apply_z_profile_knots, and the float sampling
helpers sample_polar_controls (which still expects pre-clamped jitters),
sample_checkpoint_rings and select_voronoi_anchors. The registered generators are
Stages that no longer call them (they launch the array-reading twins). They go at 1.0,
except the sampling helpers’ *_per_env forms.
The old private registries¶
Plugins written for 0.1 imported track_gen._src.generator_registry (or
gate_generator_registry) and called its register(spec) at import time. That keeps
working: register, get and available delegate to track_gen.plugin.registry,
and GENERATORS / GATE_GENERATORS are read-only views of it. Each old register
keeps its old duplicate rule. The track registry’s still overwrites an existing name (it
is registry.add(spec, replace=True)). The gate registry’s still raises when a
different module registered the name (now DuplicateSpecError, still a
ValueError) and overwrites a spec from the same module. Either emits a
DeprecationWarning when it overwrites. Such a plugin must still be imported before
use (TrackGen’s own generator modules no longer register when imported: the registry
calls their register(reg)). Move it to an entry point and registry.add to drop
that requirement (Packaging a Plugin). The shims and the built-in generators’
.params alias go at 1.0 (Removed in 1.0, and what did not change).