Repository navigation
Add csdl.experimental.compress for JAX-compiled subgraph operations - #51
Merged
Merged
Conversation
compress(inputs, outputs) moves the operations between the given variables
into one JaxCompressedOperation, a CompressedOperation subclass that:
- evaluates inline (recorder.inline, PySimulator) with one jax.jit function
instead of executing the subgraph op by op;
- differentiates with CSDL's own reverse mode, recorded into a copy of the
subgraph and wrapped as another JaxCompressedOperation, so the derivative
is one compiled node, custom and implicit operations keep their CSDL
derivatives, and higher derivatives work;
- by default (compile_separately=True) runs under JaxSimulator through a host
callback as its own executable on its own device, which keeps it out of
the simulator's XLA compile; with False it is traced into that program.
Options: absorb_feeders, intermediates ('none', 'used', 'all'), device, and
jit_kwargs. Backend-matrix tests are TestJaxCompress in
compress_operations.py; option, error, and exact-match tests are in
src/operations/tests/test_compress.py.
Compressing a region inside a loop body (frange or enter_loop, with and without the loop index, fused and separate) matches the uncompressed loop inline and under JaxSimulator, and the body holds one compressed operation. TestJaxCompress gains the same case for the backend matrix.
- share_compiled (default True): a new JaxCompressedOperation reuses the compiled function of an earlier identical one in the recorder (Recorder.compressed_operations). Regions are identical when a structural walk pairs their operations port by port (class, shapes, wiring, constant values) and one JAX trace of each region, in paired order, gives the same jaxpr. Inputs may be in a different order. - share_with=op: reuse op's compiled function explicitly; the region is checked and a mismatch raises before the graph changes. - find_repeats: also compress every other copy of the region in the active graph with the same function (greedy in topological order; op.repeats). - assume_custom_ops_match: JAX cannot see what a callback computes, so regions containing custom operations only match when asserted (share_with asserts it for that pair). - Operations sharing a function form a group; derivative operations are shared within the group by (seeded outputs, differentiated inputs), without matching, since they differentiate the same function.
JAX 0.4 and 0.6 print a pure_callback's Python function with its memory address, so regions with custom operations never matched under share_with or assume_custom_ops_match on the CI's JAX versions. Whether a callback may match is decided separately, so the address carries nothing.
msperryucsd
approved these changes
Oct 9, 2026
msperryucsd
left a comment
Collaborator
There was a problem hiding this comment.
Looks good to me. As we discussed, we should consider adding the CSDL-native hashing mechanism to remove the jax_pr dependency.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human TLDR: Automatically wrapping sections of a CSDL model in a jax function, like we previously discussed with a custom operation, except this uses the CompressedOperation class instead. By default it will put that inside a jax pure_callback, meaning it's compiled completely separately. Seems to work pretty well, and save a decent amount of compile time in the toy cases I've tested on.
Adds
csdl.experimental.compress(inputs, outputs, ...). It replaces the operations betweeninputsandoutputswith oneJaxCompressedOperationthat runs as a single jitted JAX function and is still differentiable to any order.The main motivation is long JAX compile times and compile memory on large models. By default each compressed region compiles on its own, outside
JaxSimulator's program, and can run on its own device.What it does
compressfinds the operations betweeninputsandoutputswithGraph._get_intersection, moves them into a subgraph withGraph.extract_subgraph_nodes, and attaches one operation that produces the same outputVariableobjects. Downstream operations, constraints, objectives, and simulator handles keep working. Any other variable the region reads, such as a parameter, becomes an extra input read from the graph, so nothing is frozen. Only literal constants (csdl.Constant) are compiled in.JaxCompressedOperationsubclassesCompressedOperation.CompressedOperationitself is unchanged, so its inline path still does not need JAX. The subclass differs in three ways:CompressedOperationJaxCompressedOperationPySimulatorjax.jitfunctionJaxCompressedOperation: one compiled node, itself differentiableJaxSimulatorcompile_separately=True);Falsetraces it inDerivatives use CSDL's own reverse mode, not
jax.vjp. So custom operations use theircompute_derivatives(JAX cannot differentiate their host callbacks), implicit operations use their solver's implicit derivative (JAX cannot reverse-differentiate the solver'swhile_loop), and Hessians work. The derivative operation inherits the forward operation's options.Options
absorb_feedersFalseintermediates'none''none': onlyoutputs, and an intermediate used outside the region raises.'used': add those intermediates as outputs.'all': every variable the region computes is an outputdevice'cpu','gpu', or ajax.Devicejit_kwargs{}jax.jitcompile_separatelyTrueJaxSimulator, call the operation through a host callback (backends.jax.utils.host_callback) instead of compiling it into the simulator's programshare_compiledTruefind_repeatsFalseop.repeats)share_withNoneassume_custom_ops_matchFalseWhy separate compilation is the default
With
compile_separately=True, the simulator's program contains only a callback, so its XLA compile excludes the region, and the region runs on its owndevice. Each call costs a round trip through host memory and loses XLA optimization across the boundary. Undervmap(csdl.derivative(..., loop=False)), the callback runs once per batch element.On a chain of
tanh/sinlayers split into 100-layer compressed blocks (CPU,JaxSimulator; setup plus firstrunandcompute_totals, then steady state):At 1200 layers the single fused program also hits an XLA-CPU slowdown on very long unrolled chains, which the separate blocks stay under, so the steady-state gap there is larger than usual. On a GPU
JaxSimulator, two separate blocks placed on'gpu'and'cpu'each ran on their own device and matched the uncompressed model.Note - Fused blocks means
compile_seperately=False, so we essentially end up with the same Jax function as uncompressed (hence the numbers are essentially the same).Sharing compiled functions between identical regions
Identical regions (repeated components, blocks of the same layers) can share one compiled function, so with
compile_separatelyeach distinct region compiles once however many copies there are.jax.make_jaxpr, running the paired operations in the same order, and the jaxprs must be equal. That catches parameters the walk cannot see, such as reshape shapes or exponents. Inputs may be in a different order; the operation reorders its arguments to the shared function.assume_custom_ops_match=True, or for the pair named byshare_with, which asserts it.share_compiled, checked against earlier compressed operations inRecorder.compressed_operations), explicit (share_with=op), and search (find_repeats, which finds the other copies in the graph). Withfind_repeats, overlapping copies (a two-sinregion in a chain ofsins) are taken greedily in topological order, which gives the most copies when overlaps line up like a chain;share_withgives exact control.Same chain benchmark, now as identical 100-layer blocks with
compile_separately=True(CPU,JaxSimulator; graph building, firstrun, and firstcompute_totals, which include compiling; then steady state):find_repeatsWith sharing, one forward and one derivative function compile regardless of the block count; what remains is CSDL graph and derivative recording plus about 0.04 s of matching per block.
Tests
TestJaxCompressincompress_operations.pyusesCSDLTest: values against NumPy references, and finite-difference derivatives, for each option, intermediates, a Newton implicit solve, a custom operation, a loop, nesting, compressing insidefrangeandenter_loopbodies, and the docstring example. It passes in all eight combinations ofrun_test_matrix.sh.src/operations/tests/test_compress.pycompares compressed and uncompressed models exactly, for gradients, Hessians, both simulators in both compile modes, the option validations and errors, device andjit_kwargsinheritance, the callback replacing the region in the traced program, and compressing insidefrangeandenter_loopbodies (with and without the loop index, both compile modes).test_compress.py, plus aCSDLTestcase forfind_repeatsandshare_with): separatecompresscalls, reordered inputs,share_with(including mismatches, custom operations, second derivatives), regions differing in a parameter or constant not sharing, custom operations sharing only when asserted,find_repeats(copies, overlapping chains, escaping intermediates, outputs on independent paths,JaxSimulatorin both compile modes), derivative operations sharing without any matching or tracing, and each shared function compiling once.mainwith NumPy 2 and are unrelated (test_variable.py::test_docstringsexpectsTrueand getsnp.True_;test_custom_implicit.py::test_custom_implicitraises "setting an array element with a sequence").Notes
compressuses private graph and recorder methods:Graph._get_intersection,Graph._delete_nodes, andRecorder._enter_subgraph.Recordergains one attribute,compressed_operations, the per-recorder registry used for automatic sharing.csdl.derivativecall records the derivative into a copy of the region's graph. Operations in a sharing group, and repeated derivative calls with the same seeded outputs and differentiated inputs, reuse one compiled derivative function. That function is the first member's, so a member's derivative values can differ from its own recorded graph's in the last bits (summation order). The derivative recomputes the forward values it needs; when fused, XLA removes the duplicate work.compressinside a loop body (csdl.frangeorenter_loop) is tested; inside a composed operation it is untested.