Warp Compile Time Optimizer
NVIDIAOfficialCreated by Warp TeamDistributed by NVIDIA
- Skill
- Workflow
- Software development
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to identifying and resolving excessive compile times or startup delays in Warp-based applications. It focuses on isolating compilation bottlenecks, detecting redundant module builds, and optimizing module configuration to ensure efficient kernel generation. The process relies on a dedicated probe script to measure cold-start performance and identify specific mechanisms causing compilation churn, such as identity changes, redundant module options, or inefficient module boundaries.
What it does
- Measures cold and warm compilation times using an isolated probe script.
- Identifies redundant module builds caused by identity churn or configuration changes.
- Provides guidance on aligning module lifecycles and options to prevent unnecessary recompilation.
- Helps distinguish between structural compilation issues and runtime performance bottlenecks.
- Offers a protocol for verifying optimizations through comparative analysis of launch topology and timing.
- Supplies a framework for reporting measured improvements and trade-offs.
When to use it
- When an application experiences significant delays before the first kernel launch.
- When Warp modules are observed to recompile repeatedly across runs or CI jobs.
- When optimizing startup latency for production environments.
- When investigating why specific kernels or modules trigger unexpected build activity.
Advantages
- Provides objective, data-driven insights into compilation costs.
- Minimizes noise by isolating cache directories during measurement.
- Offers clear, mechanism-based diagnostics to target specific build inefficiencies.
- Ensures that optimizations preserve existing workload behavior and numerical accuracy.
Usage examples
- “My application stalls for several seconds at the first wp.launch. How can I find out why?”
- The agent will guide you to use the warp_compile_probe.py script to measure your command, identify which modules are recompiling, and check for identity churn or configuration issues.
- “I suspect my Warp kernels are recompiling every time I run my tests. How do I fix this?”
- The agent will help you analyze the probe output to determine if the cache is being reused correctly or if module options are being set after the module has already loaded.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/nvidia/warp-compile-time-optimizer/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip warp-compile-time-optimizer.zip -d .agents/skills
Documentation by Warp Team
Warp cold-start compile time
Start with a runnable command
The probe runs the target in a subprocess, writes only to temporary directories, and needs no network, external tool servers, or Warp checkout.
| Script | Purpose | Arguments |
|---|---|---|
scripts/warp_compile_probe.py | Measure isolated cold/warm compilation and launches. | measure [OPTIONS] -- COMMAND...; use --help. |
Use run_script("scripts/warp_compile_probe.py", args=[...]) when supported;
otherwise use the Python command below. The target must run to completion.
Compilation model
Warp compiles modules, not individual kernels. A module's identity is:
(live kernel & function set) x (module options) x (CUDA block_dim) x (generic instances)Each identity requires code generation and native compilation for the full module.
Cold-start cost is roughly:
number of distinct module identities you touch x size of each moduleReduce it in two ways:
Stop identity churn. A module that changes after loading compiles again.
Stop source duplication. A module used at three block dimensions compiles every kernel three times.
Deleting one kernel from a module that still builds saves only part of one build. Removing an unnecessary module identity saves the full build.
When neither applies, overlap independent CUDA builds (CS-13). This changes when work happens, not how much is compiled, so judge it on elapsed time.
When a module's options are fixed
Options have two deadlines:
Module creation (Python import). The module copies
enable_backward,max_unroll,lineinfo,deterministic,deterministic_max_records, andcompile_time_tracefromwarp.config. Setting a global later is silently ignored by that module.default_grid_strideis the exception.First load. Before compilation, change the existing module with
wp.set_module_options()orwp.get_module(name).options. Changing an option after load creates a new identity and rebuilds the module (CS-3).
| Missed deadline | Symptom | Cost |
|---|---|---|
wp.config.* set after import | hash unchanged, option silently absent | the entire benefit, invisibly |
| module options set after load | a second hash, module builds twice | one extra build, visible in the trace |
After changing an option, confirm the hash moved for every target module. An unchanged hash means the option never arrived.
Preserve behavior
Change how Warp compiles the code, not the workload.
Do not delete or merge kernels to claim a gain. Apparently redundant stages may preserve ownership, aliasing, retained outputs, numerical boundaries, or API behavior. Fix duplication at the module level.
Preserve every launch and its order, dimensions, dtypes, devices, block dimensions, gradients, numerical modes, dynamic/plugin behavior, and public API signatures. Keep kernel names when moving definitions to module scope because logs, cache artifacts, and external tools expose them.
Instructions
1. Find the real cost, and confirm it is compilation
Ask what command the user actually waits on, then measure it cold:
python scripts/warp_compile_probe.py measure --samples 3 \
--json baseline.json -- <the user's command>The probe gives each sample private WARP_CACHE_PATH, WARP_CACHE_ROOT, and
CUDA_CACHE_PATH directories, enables module timers, and records launches. It
creates those directories under the system temporary directory and removes
them itself, so isolating a sample never requires writing a cache into the
project or deleting anything to re-measure. Isolate a hand-rolled sample the
same way. Never clear a live cache with wp.clear_kernel_cache() or
wp.clear_lto_cache(); clearing is not isolated and can disrupt other
processes.
Read the probe output before source. If compilation is a small part of wall time, report the real bottleneck and stop. For libraries and tests, use the smallest command that compiles the workload's modules.
Modules that each compiled once, with no repeated hashes, block-dimension variants, or LTO, have no structural churn. This rules out redundant builds, not oversized builds; still check cache reuse (CS-2), backward codegen (CS-10), unrolling (CS-11), the precompiled header (CS-12), and overlap when several CUDA modules remain (CS-13).
Every sample also re-runs the command against the cache it just populated. If warm module work is not near zero, diagnose cache reuse (CS-2) before changing module structure.
2. Ask about runtime tradeoffs when needed
Apply ordering fixes, lifecycle grouping, and option hoists without asking.
Ask before changing fast_math, max_unroll, or a MathDx/tile implementation:
Some of these knobs cut compile time but can make the compiled kernels slower or change numerics. Are you optimizing a fast edit-run loop (where slower kernels are usually fine), or production startup (where they usually are not)?
If the user is unavailable:
Leave numerics-changing options and implementation swaps alone.
Before removing a capability such as backward codegen, test whether it is used; report what is removed, the measured benefit, and how to revert.
Set global
wp.config.*options at application entry points, not in library code.
Record declined options and their measured benefits in the step 6 ledger.
Match the scope of the change to the scope of the evidence
A profile supports a change to the measured application, not every consumer of a shared library. Repository searches also miss out-of-tree and future callers. For example, a forward-only application does not justify disabling gradients inside a solver library that another application differentiates through.
Scope the option to the measured process, before importing the library:
import warp as wp
wp.config.enable_backward = False # must precede the library import
import the_libraryThis also reaches every module the application loads. CS-10 covers the silent import-order trap. If only a library change works, send its maintainers the measurement and let them decide the contract.
3. Diagnose from the measurement, not from reading the source
The probe prints every compiled module identity with its name, hash, device, and block dimension, then names which modules built more than once. Match what you see:
| What the probe shows | What it means | Where to look |
|---|---|---|
| One module name, several hashes | Identity churn: its kernel set, options, or generic instances changed after it first loaded | CS-1, CS-3, CS-6 |
| One module name, several block_dim values | The whole module is recompiled per block dimension (CUDA) | CS-5 |
| Many one-kernel modules in one feature | Fixed per-module cost repeated | CS-4 |
| A hash-named module per kernel | module="unique" used on stable kernels | CS-9 |
Big gap between module time and native compile time, plus .lto artifacts | MathDx/LTO setup | CS-7 |
(compiled) on a run that should have been warm | Cache is not being reused | CS-2 |
| Modules load, then "Failed to find module" | Concurrent CPU JIT first-use race | CS-8 |
| Large generated source, no rebuild problem | Unroll budget | CS-11 |
| Adjoint code in a module nothing differentiates | Backward codegen | CS-10 |
| Compiles slow across the board, or a few small modules on CUDA below toolkit 13 | The precompiled header is turned off, or is not paying for itself | CS-12 |
Several independent modules, each built once, overlap_factor near 1.0 | Builds are running one at a time; parallel loading is off by default | CS-13 |
| An option you set changed nothing, and that module's hash is unchanged | It was assigned after the module was created, so it never arrived | "When a module's options are fixed" |
| No row above fires | Nothing is being built redundantly; the cost is the size of the builds themselves | Step 6 |
references/mechanisms.md has one section per mechanism: how to confirm it,
the fix, its limits, and its failure mode. Read only the sections selected by
the measurement.
4. Choose module boundaries deliberately
Group kernels in one module only when they share:
lifecycle: they are defined, loaded, and invalidated together;
option set: they need the same
fast_math,enable_backward,max_unroll, and MathDx settings;stable block dimension on CUDA.
Kernels with the same lifecycle but different stable block dimensions should not share a module because each would compile twice. Separate kernels with independent lifecycles too.
Kernels whose block dimension varies at runtime (chosen from input size, say) have no stable mapping, so keep them in their own module rather than dragging a whole shared module into an extra variant.
Prefer the least invasive change that removes a build. Ordering fixes and option hoists are cheaper and safer than re-architecting module layout; regroup only when fixed per-module cost or block-dimension duplication dominates.
wp.set_module_options() targets its calling Python module, not kernels with
an explicit module="pkg.name". Either use a real Python module or update the
named module before it loads:
wp.set_module_options({"enable_backward": False}) # at module scope
wp.get_module("pkg.name").options.update({"enable_backward": False})Do not pass wp.get_module() to wp.set_module_options(module=...), or use
@wp.kernel(module_options={...}) without module="unique". Per-kernel
enable_backward=False has a tile-module exception covered by CS-10. Confirm
the module hash after every option change.
5. Verify
python scripts/warp_compile_probe.py measure --samples 3 \
--json candidate.json -- <the same command>
python scripts/warp_compile_probe.py compare baseline.json candidate.jsoncompare rejects changed launch topology and treats a result inside
max(1% of baseline, 2 x baseline MAD) as inconclusive.
For BUILDS OVERLAPPED, judge scheduling changes on compile elapsed rather
than summed module timers. The required warm pass supplies that clock. See
references/measurement.md.
Then check what the probe cannot see:
Diff numeric output and run the project's tests or entry points.
After changing
enable_backwardor boundaries, verify a gradient path.After changing
fast_math,max_unroll, MathDx, or an implementation, benchmark steady-state runtime.Exercise uncovered dynamic kernels, dtypes, profiles, and modes.
6. Report results
Read "Reporting results" in references/measurement.md. Report:
Describe every optimization in plain language: name the behavior, the evidence,
and the effect. For example, write "moved module options before the first load
to avoid a redundant rebuild," not "applied CS-3." Treat CS-* labels as
internal navigation aids, not user-facing explanations.
| Option | Measured | Why not taken | To take it |
|---|---|---|---|
enable_backward=False on pkg.solver | −38% cold | a live tape traverses these kernels | set at the entry point, then re-check adjoints |
max_unroll=4 | −2%, inside noise | changes generated code for no measured gain | — |
before/after medians and sample counts;
the reduction and residual cost, sized against the original complaint;
mechanisms fixed, ruled out, measured and declined, or not reached;
tradeoffs and behavior not verified;
the ledger above for declined or incompletely verified options;
the measured value of relaxing any constraint that blocked a fix.
State only what the evidence supports. "No structural churn" does not mean "optimal" or "irreducible." Measure declined levers when practical; label any estimate untested. Before reporting no available fix, check CS-13. For a possible module split, first measure a one-kernel module with the same options to establish the repeated fixed cost.
Troubleshooting
Run the target command directly before debugging the probe. See
references/measurement.md for cache/noise issues and
references/mechanisms.md for mechanism-specific failures.
Limitations
Two rules override any gain:
Isolate both Warp and CUDA caches for every cold sample.
Keep
max_workers <= 1when a load can target CPU, includingdevice=Noneand mixed device lists. Concurrent CPU first loads can lose kernels; retries do not make them safe. CUDA-only loading is unaffected.
Measurements are environment-specific: cold times move with CPU, GPU, driver,
toolchain, and Warp version. Mechanisms transfer; numbers do not. Read the
known unknowns in references/mechanisms.md before making broad claims.
Reference files
references/mechanisms.md: the thirteen compile-time mechanisms, each with its confirming signal, fix, applicability limits, and failure mode. Read the sections your measurement points to.references/measurement.md: measurement protocol, what each metric does and does not mean, reporting guidance, log examples, and manual measurement.
Files
- ATTRIBUTION.mdAdded by AutoGineer942 B
- BENCHMARK.md4.3 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md13.7 KiB
- config/skillspector-baseline.yaml903 B
- evals/config.yml1.3 KiB
- evals/evals.json17.1 KiB
- evals/files/c01_particle_preprocessing/pyproject.toml262 B
- evals/files/c01_particle_preprocessing/src/particle_prep/__init__.pyScript309 B
- evals/files/c01_particle_preprocessing/src/particle_prep/__main__.pyScript731 B
- evals/files/c01_particle_preprocessing/src/particle_prep/pipeline.pyScript2.2 KiB
- evals/files/c01_particle_preprocessing/src/particle_prep/plugins.pyScript1.2 KiB
- evals/files/c01_particle_preprocessing/src/particle_prep/stages.pyScript1.4 KiB
- evals/files/c04_batch_signals/pyproject.toml332 B
- evals/files/c04_batch_signals/src/batch_signals/__init__.pyScript336 B
- evals/files/c04_batch_signals/src/batch_signals/__main__.pyScript820 B
- evals/files/c04_batch_signals/src/batch_signals/conditioning.pyScript2.1 KiB
- evals/files/c04_batch_signals/src/batch_signals/maintenance.pyScript691 B
- evals/files/c04_batch_signals/src/batch_signals/runtime.pyScript1001 B
- evals/files/g01_pde_stepper/pyproject.toml260 B
- evals/files/g01_pde_stepper/src/pde_stepper/__init__.pyScript273 B
- evals/files/g01_pde_stepper/src/pde_stepper/__main__.pyScript784 B
- evals/files/g01_pde_stepper/src/pde_stepper/boundaries.pyScript1.5 KiB
- evals/files/g01_pde_stepper/src/pde_stepper/operators.pyScript2.2 KiB
- evals/files/g01_pde_stepper/src/pde_stepper/solver.pyScript2.9 KiB
- evals/files/g02_stencil_bank/pyproject.toml261 B
- evals/files/g02_stencil_bank/src/stencil_bank/__init__.pyScript303 B
- evals/files/g02_stencil_bank/src/stencil_bank/__main__.pyScript684 B
- evals/files/g02_stencil_bank/src/stencil_bank/analysis.pyScript2.0 KiB
- evals/files/g02_stencil_bank/src/stencil_bank/loss.pyScript1.9 KiB
- evals/files/g02_stencil_bank/src/stencil_bank/stencils.pyScript1.3 KiB
- evals/files/g04_image_pyramid/pyproject.toml262 B
- evals/files/g04_image_pyramid/src/image_pyramid/__init__.pyScript306 B
- evals/files/g04_image_pyramid/src/image_pyramid/__main__.pyScript690 B
- evals/files/g04_image_pyramid/src/image_pyramid/adaptive.pyScript1.6 KiB
- evals/files/g04_image_pyramid/src/image_pyramid/filters.pyScript2.6 KiB
- evals/files/g04_image_pyramid/src/image_pyramid/pyramid.pyScript2.0 KiB
- evals/files/g06_gpu_analytics/pyproject.toml336 B
- evals/files/g06_gpu_analytics/src/gpu_analytics/__init__.pyScript355 B
- evals/files/g06_gpu_analytics/src/gpu_analytics/__main__.pyScript821 B
- evals/files/g06_gpu_analytics/src/gpu_analytics/analytics.pyScript4.7 KiB
- evals/files/g06_gpu_analytics/src/gpu_analytics/diagnostics.pyScript1.3 KiB
- evals/files/g06_gpu_analytics/src/gpu_analytics/reductions.pyScript751 B
- evals/files/g08_tile_projection/pyproject.toml331 B
- evals/files/g08_tile_projection/src/tile_projection/__init__.pyScript318 B
- evals/files/g08_tile_projection/src/tile_projection/__main__.pyScript1.3 KiB
- evals/files/g08_tile_projection/src/tile_projection/maintenance.pyScript675 B
- evals/files/g08_tile_projection/src/tile_projection/session.pyScript2.2 KiB
- evals/files/g08_tile_projection/src/tile_projection/stages.pyScript1.6 KiB
- evals/files/g09_mixed_precision/pyproject.toml338 B
- evals/files/g09_mixed_precision/src/mixed_precision/__init__.pyScript277 B
- evals/files/g09_mixed_precision/src/mixed_precision/__main__.pyScript1.1 KiB
- evals/files/g09_mixed_precision/src/mixed_precision/kernels.pyScript1.7 KiB
- evals/files/g09_mixed_precision/src/mixed_precision/service.pyScript2.3 KiB
- evals/files/g10_shared_solver/pyproject.toml261 B
- evals/files/g10_shared_solver/run_calibrate.pyScript2.2 KiB
- evals/files/g10_shared_solver/run_inspect.pyScript723 B
- evals/files/g10_shared_solver/src/field_solver/__init__.pyScript436 B
- evals/files/g10_shared_solver/src/field_solver/calibrate.pyScript2.3 KiB
- evals/files/g10_shared_solver/src/field_solver/inspect_field.pyScript2.5 KiB
- evals/files/g10_shared_solver/src/field_solver/kernels.pyScript5.5 KiB
- evals/files/g11_stage_pipeline/pyproject.toml263 B
- evals/files/g11_stage_pipeline/run_pipeline.pyScript6.7 KiB
- evals/files/g11_stage_pipeline/src/stage_pipeline/__init__.pyScript508 B
- evals/files/g11_stage_pipeline/src/stage_pipeline/geometry.pyScript1.8 KiB
- evals/files/g11_stage_pipeline/src/stage_pipeline/reduction.pyScript1.8 KiB
- evals/files/g11_stage_pipeline/src/stage_pipeline/spectral.py5.2 KiB
- evals/files/g11_stage_pipeline/src/stage_pipeline/statistics.py2.3 KiB
- evals/files/g11_stage_pipeline/src/stage_pipeline/transport.py1.9 KiB
- evals/files/g12_dense_stage/pyproject.toml260 B
- evals/files/g12_dense_stage/run_stage.py2.6 KiB
- evals/files/g12_dense_stage/src/dense_stage/__init__.py262 B
- evals/files/g12_dense_stage/src/dense_stage/kernels.py5.6 KiB
- evals/files/triton_service.py1.8 KiB
- references/measurement.md12.6 KiB
- references/mechanisms.md28.8 KiB
- scripts/warp_compile_probe.py38.7 KiB
- skill-card.md4.0 KiB
- skill.oms.sig23.9 KiB
references/mechanisms.md
Warp cold-compile mechanisms
Each section gives a signal, cause, fix, limits, and measured result. Read only the sections selected by your measurement.
Contents
| ID | Problem | Backend |
|---|---|---|
| CS-1 | Kernels defined after their module loaded | CPU + CUDA |
| CS-2 | The cache is never reused | CPU + CUDA |
| CS-3 | Module options set after first load | CPU + CUDA |
| CS-4 | Module boundaries too granular | CPU + CUDA |
| CS-5 | One module spans several CUDA block dimensions | CUDA |
| CS-6 | Generic instances appear after first load | CPU + CUDA |
| CS-7 | MathDx LTO without an amortization case | CUDA tiles |
| CS-8 | Concurrent first CPU load races the native JIT | CPU |
| CS-9 | module="unique" on stable kernels | CPU + CUDA |
| CS-10 | Backward code generated but never used | CPU + CUDA |
| CS-11 | Unroll budget inflates source | CPU + CUDA |
| CS-12 | The precompiled header is not paying for itself | CPU + CUDA |
| CS-13 | Independent modules are built one at a time | CUDA |
How to read the numbers below
Every figure comes from one controlled study, on one machine, with one Warp revision. Use them to rank mechanisms and to sanity-check whether a result is plausible. Absolute milliseconds do not transfer between machines.
Percentages do not transfer either, and for some mechanisms they cannot: where the payoff is set by the shape of the program rather than by the fix, one measurement says nothing about the next application. Those sections say so and give you a way to bound your own case from your own measurement instead. Do not quote a figure from this file to a user as the result they should expect.
On CPU, a large fixed native-toolchain cost dominates each run, so the same structural fix shows a small relative gain on CPU and a large one on CUDA. Do not conclude a CPU fix failed because the percentage is modest, and do not extrapolate a CUDA percentage to CPU.
Part of that CPU floor is Warp's precompiled header build, which is what keeps later modules cheap. Check the header first (CS-12) — it can be wrong in either direction, and it applies on CUDA too — then focus on structural mechanisms.
CS-1: Kernels defined after their module loaded
Signal. One module name, several hashes. A Module hash changed, recompiling line in verbose output.
Why. A module's hash covers its live kernel set. Define a kernel, launch it (the module builds), then define another kernel in the same module and launch again. The new kernel changes the identity, so Warp builds the module again with both kernels.
Fix, in order of preference:
Move stable kernel definitions to module scope.
If they must be created at runtime, create every expected kernel in that module before the first launch.
Give kernels that share one lifecycle a deliberately named module.
Only then,
module="unique"for a kernel that cannot join a stable module.
# Before: the module builds once for `first`, then again for `first` + `second`.
def run(values):
@wp.kernel
def first(x: wp.array[wp.float32]):
x[wp.tid()] += 1.0
wp.launch(first, dim=values.shape, inputs=[values])
@wp.kernel
def second(x: wp.array[wp.float32]):
x[wp.tid()] *= 2.0
wp.launch(second, dim=values.shape, inputs=[values])# After: one identity, one build.
@wp.kernel
def first(x: wp.array[wp.float32]):
x[wp.tid()] += 1.0
@wp.kernel
def second(x: wp.array[wp.float32]):
x[wp.tid()] *= 2.0
def run(values):
wp.launch(first, dim=values.shape, inputs=[values])
wp.launch(second, dim=values.shape, inputs=[values])Does not apply when runtime data creates a kernel per call, such as a user-supplied expression, plugin, or varying shape specialization. Keep these kernels isolated so they do not invalidate a shared module.
Observed. Total cold module work fell about 20% on CUDA and about 6% on CPU; generated source roughly halved. Creating the dynamic kernels upfront scored the same as module-scope definition.
CS-2: The cache is never reused
Signal. A run that should have been warm still reports (compiled), or the
configured cache directory is empty at process start. The probe's warm pass makes
this visible on every measurement: a repeat run should cost near zero.
Why. Warp cannot reuse a module binary without its cache. CUDA also has a driver cache.
Check application code for:
wp.clear_kernel_cache()orwp.clear_lto_cache()left in application code. These calls are not safe across processes.wp.config.cache_kernels = False, which disables reuse outright.wp.config.kernel_cache_dirpointed at a fresh temporary directory per run, or at a path containing a timestamp, so every invocation starts empty.The same directory removed on exit, by
shutil.rmtreeor anatexithook.CUDA_CACHE_DISABLEset in the environment, which defeats the driver-side layer even when the Warp cache is healthy.
Fix. Persist a cache directory scoped to a compatible application build,
Warp version, module options, target architecture, and driver/toolchain. On
CUDA persist CUDA_CACHE_PATH as well as WARP_CACHE_PATH. Where persistent
storage is impossible, prewarm during startup and report that the cost moved.
For per-request containers, CI jobs, and serverless handlers, a persisted cache
can reduce module work to near zero.
Does not apply when the caller needs a clean build, or the cache would be shared across incompatible builds, architectures, drivers, or option sets. Reusing such artifacts is unsafe.
Observed. Cold module work of several hundred milliseconds (CUDA) and several seconds (CPU) fell to roughly 1 ms warm. Process startup and imports remain regardless.
CS-3: Module options set after first load
Signal. A build, then a hash change and another build, with a
wp.set_module_options() call in between.
Why. Options are part of module identity. Changing one after the module has loaded invalidates it.
This is the later of the two option deadlines described under "When a module's
options are fixed" in SKILL.md, and the more forgiving one: it costs an extra
build and announces itself as a second hash. The earlier deadline, assigning
wp.config.* after the module's file was imported, produces no build and no
symptom at all. If an option appears to have been ignored rather than applied
late, check that one instead.
# Before
wp.launch(first, ...)
wp.set_module_options({"fast_math": True}) # invalidates the module
wp.launch(second, ...)
# After
wp.set_module_options({"fast_math": True}) # resolved before anything loads
wp.launch(first, ...)
wp.launch(second, ...)Fix. Resolve mode, fast_math, enable_backward, max_unroll, and MathDx
flags before the module's first load. Set them at module scope or in an init
path before the first launch.
Check the call site of helpers such as configure_stages() or setup_mode().
Also check which module a bare wp.set_module_options() applies to: it targets
the calling Python module, which is not necessarily where the kernels live.
Does not apply when kernels need different option sets. Then the fix is separate named modules by option set (CS-4).
For a named shared module, @wp.kernel(module_options={...}) is rejected
unless the kernel is module="unique". wp.set_module_options() resolves the
calling Python module's __name__; it accepts neither a module-name string nor
a wp.Module. Use one of these patterns:
give the group a real Python module and call a bare
wp.set_module_options()at module scope; orset
wp.get_module("pkg.name").options.update({...})while the module is still empty, before any kernel in it loads.
The first is clearer; the second avoids adding files.
Observed. A late fast_math change cost roughly half of total cold module
work on CUDA (two full builds instead of one); about 6% on CPU.
CS-4: Module boundaries too granular
Signal. One feature loads and emits many small module events with similar lifecycles.
Why. Every module carries its own hash, generated source, compiler invocation, metadata, and binary load. Small modules repeat that fixed cost.
Fix. Group kernels that are deployed, loaded, and invalidated together into one named module. Choose boundaries by lifecycle, not by a kernels-per-module rule.
Does not apply when a kernel changes independently, is rarely used, needs different options, or on CUDA uses a different stable block dimension. Merging kernels with different block dimensions makes each compile at both dimensions.
A broader module also recompiles more code when any member changes, so aggressive merging can slow down the edit-run loop it was meant to help.
Observed. Four one-kernel modules merged into one shared module cut cold module work by about 29% on CUDA and 9% on CPU.
CS-5: One module spans several CUDA block dimensions
Signal. The same module name compiled at several block_dim values. The
generated .cu for each variant contains the module's whole live kernel set.
Why. Warp compiles a module as a unit for an active block_dim. Every live
kernel in it is generated and compiled at every block dimension requested,
even a kernel only ever launched at one of them.
Fix. When the kernel-to-block-dimension mapping is stable, put each block dimension's kernels in their own named module. Alternatively, reduce the number of requested block dimensions if the launches were not deliberately tuned.
Does not apply when the mapping is not stable. A kernel whose block dimension is chosen from input size at runtime has no fixed home. Keep it in its own module rather than pulling a shared module into extra variants. It also does not apply on CPU, where the effective block dimension is 1; splitting there adds modules and made things worse in measurement.
Do not "fix" this by collapsing kernels that share a body into one kernel launched at a single block dimension. That changes the workload.
Observed. Partitioning three stable block-dimension groups cut generated source by about 60% and cold module work by about 13% on CUDA. The same split on CPU was a regression.
CS-6: Generic instances appear after first load
Signal. Two launches of the same generic kernel with different types produce two module hashes.
Why. Instantiating a new overload changes the module's live generic kernel set, so the next launch rebuilds it.
# After: register the types this code is known to use, before the first launch.
scale_f32 = wp.overload(scale, [wp.array[wp.float32], wp.float32])
scale_f64 = wp.overload(scale, [wp.array[wp.float64], wp.float64])
wp.launch(scale, dim=x32.shape, inputs=[x32, wp.float32(2.0)])
wp.launch(scale, dim=x64.shape, inputs=[x64, wp.float64(2.0)])Retain the returned overload objects; letting them be collected defeats the purpose.
Fix. Call wp.overload() for the type signatures the application routinely
uses, before the first launch.
Does not apply to rare or diagnostic types. Registering a specialization a run never uses adds work. Two independent runs on a bank of five specializations where the workload needed two found that registering all five upfront roughly doubled cold module work (848 ms and 844 ms, versus 414 ms and 424 ms for registering only the two in use). Once the unused set is large, the extra generated source outweighs the fixed per-module cost you were trying to amortize.
Register routine types early. Leave rare specializations lazy or put them in a module with a separate lifecycle.
Observed. About 17% less cold module work on CUDA, 5% on CPU.
CS-7: MathDx LTO without an amortization case
Signal. .lto artifacts in the cache, plus a large gap between total module
time and native compiler time. The remainder is dominated by LTO work.
Why. MathDx-backed tile operations (tile_matmul, tile_cholesky,
tile_fft, ...) generate and link LTO material at first compile. That cost is
large and paid once per cold identity.
An adjoint tile_matmul needs its own linked GEMMs, so a forward tile operation
with backward enabled produces roughly three links instead of one. Check
enable_backward first (CS-10).
Fix. Set the relevant enable_mathdx_* option both ways and measure build
time and steady-state kernel time.
One recent architecture produced these tile GEMM results:
| Build | Steady state | Results | |
|---|---|---|---|
| float32 | 15-25x more expensive with MathDx | no faster, marginally slower | bit-identical |
| float16 | ~14x more expensive with MathDx | ~2.9x faster with MathDx | differ slightly |
On this float32 path, disabling MathDx saved build time without a runtime loss. On float16, disabling it caused a runtime regression. Measure the active precision.
A wrapper that allocates device arrays and returns NumPy can spend several times the kernel's own cost on transfers. In one case, wrapper overhead hid a 2.9x kernel slowdown. Time the launch or make wrapper overhead small relative to the work.
If startup and per-request paths need different options, put them in separate modules.
Does not apply when the MathDx path's runtime behavior is required and you have measured its benefit.
Observed. The enabled path's cold module work was roughly an order of magnitude above the fallback for one tile GEMM, with most of the difference outside the native compiler timer. Warm cost was also markedly higher.
CS-8: Concurrent first CPU load races the native JIT
Signal. Modules report a successful load, then Failed to find module or
Failed to find forward kernel at launch. It may not reproduce every run.
Why. Concurrent first-use CPU Module.load() calls reach a native JIT
creation path that publishes process-global pointers without synchronization.
Two callers can create different JIT instances; a module registered in the one
that gets overwritten becomes unreachable.
Fix. Use max_workers <= 1 for any module load that can target the CPU:
device="cpu", device=None, and mixed device lists all count.
wp.load_module(package, device="cpu", recursive=True, max_workers=1)Do not mask this with launch retries or JIT prewarming. Both can hide a correctness failure without making the native path safe. There is no evidence-backed concurrent CPU fallback today.
CUDA-only loading is not subject to this restriction. In measurement, parallel loading of four balanced CUDA modules performed about the same as serial, so treat it as unproven rather than free.
CS-9: module="unique" on stable kernels
Signal. A separate hash-named module per kernel, instead of one shared module event.
Why. module="unique" gives a kernel its own module. That isolates
invalidation, but it also means one build and one load per kernel, and
lifecycle-related kernels can no longer share fixed cost.
Fix. Same order as CS-1: module-scope definitions, then shared definitions
created before first load, then a named partition, and module="unique" last.
Watch for module="unique" appearing as a default in a kernel-factory
helper. That is where it silently spreads to stable kernels that never needed
it.
Does not apply to a kernel whose runtime captures, options, or lifetime would otherwise invalidate a useful shared module repeatedly, such as a plugin, a user-supplied transform, a per-request specialization. Keeping those unique avoids repeated shared-module invalidation.
Observed. Stable module-scope sharing beat unique-per-kernel by about 15% on CUDA. Unique modules did beat repeated shared-module invalidation.
CS-10: Backward code generated but never used
Signal. Adjoint code in the generated source for a module nothing differentiates.
Why. Backward code generation emits adjoint code and supporting source for every kernel in the module.
On a tile module, an adjoint tile_matmul needs its own linked GEMMs, so
backward generation roughly triples the number of MathDx links. Two measured
cases found it was the largest build cost. For tile operations, check this
first (CS-7).
On a tile module, use the module option. Measured on two tile kernels sharing a module:
| How backward was disabled | Module time | LTO artifacts | adj_ in .cu |
|---|---|---|---|
| not disabled | 8902 ms | 3 | 108 |
wp.set_module_options({"enable_backward": False}) | 3482 ms | 1 | 2 |
@wp.kernel(enable_backward=False) | 8958 ms | 3 | 2 |
The per-kernel argument removes adjoint source but still links adjoint GEMMs. Confirm the module option with LTO artifact count or build time.
Fix. @wp.kernel(enable_backward=False) on a forward-only kernel, or the
module option set before first load for a forward-only module.
Before disabling backward:
Test whether anything differentiates the kernel today. Wrap the entry point in a
wp.Tapeand inspectlen(tape.launches). A NumPy return does not prove that the tape missed the kernel.Check whether the public API promises gradients.
Ask who owns the module. Both checks above look at the code in front of you, which is the right question for an application and the wrong one for a library. See "Where you apply it" below.
If none applies, make backward removal explicit, reversible in one line, and attach the measured result.
Where you apply it matters as much as whether. Writing the module option into
a shared library narrows that module for every consumer, on the strength of one
application's profile. Scope the change to the process you measured by setting
the global at its entry point instead; step 2 of SKILL.md covers the reasoning.
The ordering rule is the import deadline in "When a module's options are fixed"
(SKILL.md): enable_backward is one of the six options a module copies out of
warp.config as it is created. A global set after the library import therefore
reaches only what that library imports lazily, and misses the rest with no error
and no warning. Measured on a Newton scene:
wp.config.enable_backward = False | Cold module work | newton._src.sim.articulation |
|---|---|---|
| not set | 27440 ms | hash data0137, 7803 ms |
set after import newton | 16851 ms | hash data0137, 7745 ms — untouched |
set before import newton | 10455 ms | hash 25439b5, 1313 ms |
The middle row is the trap: a third of the available benefit silently missing, because that module was already sealed at import. Verify by confirming the hash moved for every module you meant to change — an unchanged hash means the option never arrived.
Import order is not always yours to control. When the library is already imported by the time your code runs, reach the module that exists instead of the default it was built from:
wp.get_module("pkg.module").options["enable_backward"] = False # before it loadsThat is still application-scoped — it configures this process, not the library's
source — and it produced the same module identity as setting the global early
(hash 25439b5, 1352 ms). Both stop working once the module has loaded, at which
point you are in CS-3.
Does not apply when a tape demonstrably traverses the launch and the result is used. Disabling backward there does not error loudly; it makes differentiation silently wrong. Where forward-only and differentiable kernels mix, split them into separate modules rather than disabling backward broadly.
Observed. Generated source fell by roughly half. Cold module work fell about 6% on CUDA and 2% on CPU in the original study. Three small-kernel modules measured 26%, 30%, and 37% reductions.
CS-11: Unroll budget inflates source
Signal. Large generated source from static loop expansion, without a matching rebuild problem.
Why. Static unrolling emits the loop body up to the configured bound.
Fix. Use the smallest max_unroll that preserves required runtime
performance, and set it before first load.
Does not apply as a compile-time fix at all, on the current evidence. Lowering the bound cut generated source by about 39% and produced no measurable cold-time improvement. More source did not mean a slower compile here. Treat this as a source-size and runtime knob, not a cold-start fix, and do not spend a possible kernel-throughput regression to buy a compile-time improvement that measurement does not support.
If you change it, benchmark steady-state kernel time.
CS-12: The precompiled header is not paying for itself
wp.config.use_precompiled_headers defaults to True, and it is not
CPU-only: build_cuda() receives both the flag and an NVRTC header directory,
so it applies on CUDA as well, for toolkit versions below 13.0. On 13.0 and
above get_nvrtc_pch_dir() returns None because CUDA manages this itself, so
the knob stops mattering. Check the toolkit version before reasoning about it.
Signal, in either direction. Compiles are slow across the board and
something sets use_precompiled_headers = False; or the default is in force and
the workload is a handful of small modules, where building the header can cost
more than it saves.
Why it matters. Warp builds the header once per compiling thread, then each module behind it compiles far faster. That trade is good for many modules or large ones and can invert for a few small ones — and because the header is per-thread, parallel loading (CS-13) multiplies its cost by the worker count.
Check. Search the project, its entry points, and its deployment config for
use_precompiled_headers. If it is being set to False, find out why. Unless
there is a recorded measurement justifying it on this workload, restore the
default.
Do not disable it from library code. It is a global setting that changes compilation for every Warp user in the process, so it belongs at an application entry point if anywhere. Any exception requires an application-level benchmark rather than an argument.
Measured against the default. On a five-module CUDA workload with toolkit 12.9, turning the header off cut cold wall clock from 2.38 s to 1.94 s serially, and combined with a parallel preload reached 1.36 s against a 2.38 s baseline. That is the inverted case, not the general one: few modules, all small. Measure before assuming which side of the trade a workload sits on, in either direction.
Does not apply on CUDA toolkit 13.0 and above, where Warp passes no header directory.
CS-13: Independent modules are built one at a time
Every other mechanism here compiles less. This one compiles the same code in less time, so it is the only fix available when the build is already minimal — and the only one that survives a constraint against touching library code.
Signal. Several independent modules, each compiled exactly once, with no
mechanism above firing. The probe's overlap_factor is about 1.0, meaning summed
module time and compile elapsed agree, so nothing overlapped. Cost is spread
across modules rather than concentrated in one.
Why. warp.config.load_module_max_workers defaults to 0, which means
serial. Modules otherwise load lazily at first launch, one at a time, so a
workload touching eleven modules pays them end to end even though they are
independent and the machine has idle cores.
Fix. Load them concurrently before the first launch, from the application entry point:
wp.force_load(device=device, modules=[...], max_workers=min(os.cpu_count(), 8))wp.load_module(name, device=..., max_workers=N) does the same for one module by
name. Nothing about the compilation changes: same identities, same hashes, same
generated source. Only the scheduling does. This needs no library patch, which is
what makes it reachable when a pinned dependency owns the expensive modules.
Observed once. On a Newton rigid-body scene compiling 11 CUDA modules on 8 cores, preloading 8 of them took compile elapsed from 29.4 s to 18.5 s, with 189 launches, 11 module identities, and 4 280 961 bytes of generated source identical in both arms.
That figure is evidence the mechanism works. It is not a number to expect, and it should not be quoted to a user as one. What this mechanism pays out depends almost entirely on how a particular program's compile cost is distributed across its modules, and that varies far more between applications than anything else in this file. A program whose cost sits in one large module gains nothing at all; a program spread evenly across many modules approaches the worker count. Both are ordinary. There is no typical case to report.
Bound your own case before writing any code. Overlapping builds cannot finish sooner than the slowest single module, so the per-module breakdown you already measured gives you the answer's upper limit:
best possible elapsed = largest module time
ceiling speedup = sum of module times / largest module timeFor the run above: 29.3 s over 12.3 s is a 2.4x ceiling, so 58% was the most that
was available and 37% is what landed. The shortfall is structural — code
generation serializes under _codegen_lock, the surrounding Python is GIL-bound,
and those 8 cores were shared. Expect to land somewhere between that ceiling and
nothing, and report the ceiling alongside the result so the reader can see how
much of the available headroom you captured.
A ratio near 1.0 means one module is the whole cost and this mechanism has nothing to offer. Compute it first and skip the work.
Read the result on the clock, not the sum. Summed module timers went up,
29.2 s to 41.7 s, because overlapping builds contend and each module's timer
absorbs the others'. That is the metric inversion described in
measurement.md; judge this mechanism on compile elapsed.
Limits, and they are real.
CUDA only. Warp's native CPU JIT first-use path is not synchronized, so concurrent first loads can report success and then fail kernel lookup. Gate on
device.is_cuda; see CS-8.The ceiling is well under linear. Code generation is serialized under a global
_codegen_lockand the rest is GIL-bound Python, so only native compilation genuinely overlaps. A single dominant module is a hard floor: no worker count makes a 12 s module finish sooner.Never pass
modules=None. That loads every registered module, not the ones you need. In the measured case 94 modules were registered against 11 used, so the blanket form would compile roughly 8.5x the code and lose badly.A preload whose block dimension does not match the launch compiles the module twice. With no explicit
block_dim,force_loadpreloads the variants already loaded on that device, falling back to the module default when there are none — which is the situation during a prewarm, because nothing has loaded yet. So a module whose kernels are launched at a block dimension other than its module default gets built at the default first and at the real dimension second. This is easy to walk into: a five-module prewarm measured here turned five builds into nine and came out slower than no prewarm at all. Either declare each module's real block dimension as its module default,wp.set_module_options({"block_dim": N}), so an unqualified preload lands on the right variant, or passblock_dimexplicitly. Leave modules whose block dimension is chosen at runtime to load lazily.Do not swallow the error.
load_moduleraisesRuntimeErrorfor a module that is not imported or holds no Warp code. Anexcept RuntimeError: passaround the call turns a typo in a module name into a silent no-op that still looks like a fix. Let it raise, or log it.
Maintenance. A hardcoded module list names another package's internals. When that dependency reorganizes, the list degrades quietly — you lose the speed-up, you do not break. Regenerate it from a verbose cold run, and assert the list still resolves.
Does not apply when one module dominates the total, when the target is CPU, or when the cache is persisted, since a warm run has nothing to overlap.
Known unknowns
Do not present these as established:
The MathDx break-even point in launches. Not measured.
How CS-13 scales at other module counts, size balances, and worker counts. One workload was measured; the mechanism is established, the magnitude is not.
Whether cache artifacts move safely between hosts, containers, drivers, architectures, or Warp builds. Only same-host reuse was tested.
Debug mode, line info, optimization levels, and custom compiler flags. These were never compared and remain hypotheses.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Python
The author says: Requires Python 3.10+ and an installed warp-lang package. A CUDA device is needed to diagnose CUDA-specific mechanisms.
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Warp Team
- Publisher
- NVIDIA (Official)
- Distributed by
- NVIDIA
- Source
- NVIDIA/skills · skills/warp-compile-time-optimizer · commit fce3cca
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE-APACHE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Warp Team or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download warp-compile-time-optimizer.zip