@@ -78,6 +78,7 @@ def export_and_lower(
7878 backend : str = "cuda" ,
7979 sample : bool = True ,
8080 use_turboquant : bool = False ,
81+ use_offgraph_kv_cache : bool = False ,
8182 activation_dtype : torch .dtype = torch .bfloat16 ,
8283 max_prefill_chunk : int = 512 ,
8384 vision_model : nn .Module | None = None ,
@@ -92,12 +93,20 @@ def export_and_lower(
9293 output_dir ,
9394 sample = sample ,
9495 use_turboquant = use_turboquant ,
96+ use_offgraph_kv_cache = use_offgraph_kv_cache ,
9597 vision_model = vision_model ,
9698 pos_embed_table = pos_embed_table ,
9799 max_vision_patches = max_vision_patches ,
98100 vision_fp32_mm = vision_fp32_mm ,
99101 )
100102 elif backend == "mlx" :
103+ # The off-graph cache is lowered by the CUDA backend only, and dropping
104+ # the flag silently would hand back an in-graph model that looks like
105+ # what was asked for.
106+ if use_offgraph_kv_cache :
107+ raise ValueError (
108+ "--use-offgraph-kv-cache is not supported by the mlx backend"
109+ )
101110 _export_mlx (
102111 model ,
103112 config ,
@@ -119,7 +128,7 @@ def _solo_constant_methods(
119128 config : MuseGlimmerConfig ,
120129 max_prefill : int ,
121130 activation_dtype : torch .dtype ,
122- mutable_buffer_metadata : str ,
131+ mutable_buffer_metadata : str | None ,
123132 has_vision : bool ,
124133 max_vision_patches : int ,
125134) -> dict [str , object ]:
@@ -136,12 +145,13 @@ def _solo_constant_methods(
136145 "get_vocab_size" : config .vocab_size ,
137146 "get_max_prefill_chunk" : max_prefill ,
138147 "get_activation_dtype" : common .activation_dtype_tag (activation_dtype ),
139- "get_mutable_buffer_metadata" : mutable_buffer_metadata ,
140148 "use_kv_cache" : True ,
141149 "use_sdpa_with_kv_cache" : False ,
142150 "enable_dynamic_shape" : True ,
143151 "has_vision_encoder" : bool (has_vision ),
144152 }
153+ if mutable_buffer_metadata is not None :
154+ constant_methods ["get_mutable_buffer_metadata" ] = mutable_buffer_metadata
145155 if has_vision :
146156 constant_methods ["get_vision_hidden_size" ] = config .dim
147157 constant_methods ["get_max_vision_patches" ] = int (max_vision_patches )
@@ -154,6 +164,7 @@ def _export_cuda(
154164 output_dir : str ,
155165 sample : bool = True ,
156166 use_turboquant : bool = False ,
167+ use_offgraph_kv_cache : bool = False ,
157168 vision_model : nn .Module | None = None ,
158169 pos_embed_table : torch .Tensor | None = None ,
159170 max_vision_patches : int = 16384 ,
@@ -162,6 +173,9 @@ def _export_cuda(
162173 import torch ._inductor .config as inductor_config
163174 from executorch .backends .cuda .cuda_backend import CudaBackend
164175 from executorch .backends .cuda .cuda_partitioner import CudaPartitioner
176+ from executorch .backends .cuda .passes .lower_offgraph_kv import (
177+ OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC ,
178+ )
165179 from executorch .exir import (
166180 EdgeCompileConfig ,
167181 ExecutorchBackendConfig ,
@@ -191,17 +205,31 @@ def _export_cuda(
191205 from executorch .examples .models .muse_glimmer .source_transformations .cuda import (
192206 add_on_device_sampler ,
193207 cuda_source_transformations ,
208+ enable_offgraph_kv_cache ,
209+ offgraph_kv_cache_geometry ,
194210 vision_cuda_source_transformations ,
195211 )
196212
197213 # Always applied: bounds global-attention SDPA to the valid context via a
198214 # runtime kv_len (O(context) decode). With use_turboquant=True it also swaps
199215 # the global KV caches for TurboQuant TQ4.
200- cuda_source_transformations (model , use_turboquant = use_turboquant )
216+ if use_offgraph_kv_cache and use_turboquant :
217+ raise ValueError ("off-graph KV cache and TurboQuant are mutually exclusive" )
201218
202- # Max prefill chunk must fit in the ring buffer (2 * sliding_window)
219+ # A prefill chunk is one write step, so the ring must hold the union of the
220+ # step's per-query windows. enable_offgraph_kv_cache sizes it from this.
203221 max_prefill = min (config .max_seq_len - 1 , model ._sliding_window * 2 )
204222
223+ offgraph_manifest = None
224+ offgraph_geometry = {}
225+ if use_offgraph_kv_cache :
226+ offgraph_manifest = enable_offgraph_kv_cache (model , max_prefill )
227+ # Read the geometry while the model is alive; it is freed before the
228+ # constant methods are assembled.
229+ offgraph_geometry = offgraph_kv_cache_geometry (model )
230+ else :
231+ cuda_source_transformations (model , use_turboquant = use_turboquant )
232+
205233 has_vision = vision_model is not None
206234 programs : dict [str , "torch.export.ExportedProgram" ] = {}
207235
@@ -267,20 +295,31 @@ def _export_cuda(
267295 vision_model , pos_embed_table , max_vision_patches
268296 )
269297
270- mutable_buffer_metadata = common . mutable_buffer_metadata ( model )
271- del model
272- if has_vision :
273- del vision_model
298+ mutable_buffer_metadata = (
299+ None if offgraph_manifest else common . mutable_buffer_metadata ( model )
300+ )
301+ del model , vision_model
274302 gc .collect ()
275303 torch .cuda .empty_cache ()
276304
277305 def _partitioner_for (name : str ) -> "CudaPartitioner" :
278- return CudaPartitioner (
279- [
280- CudaBackend .generate_method_name_compile_spec (name ),
281- CompileSpec ("low_memory_mode" , b"ON" ),
282- ]
283- )
306+ compile_specs = [
307+ CudaBackend .generate_method_name_compile_spec (name ),
308+ CompileSpec ("low_memory_mode" , b"ON" ),
309+ ]
310+ if offgraph_manifest is not None :
311+ compile_specs .extend (
312+ (
313+ CompileSpec ("offgraph_kv_manifest" , offgraph_manifest .encode ()),
314+ # Where the delegate reads this step's token count, as
315+ # "input_index:dim". Lowering compiles the cache op away, so
316+ # the runtime cannot recover the width from the graph; every
317+ # method here takes input_pos second, shaped [T].
318+ CompileSpec (OFFGRAPH_KV_STEP_WIDTH_COMPILE_SPEC , b"1:0" ),
319+ CompileSpec ("autotune_at_compile_time" , b"OFF" ),
320+ )
321+ )
322+ return CudaPartitioner (compile_specs )
284323
285324 constant_methods = _solo_constant_methods (
286325 config = config ,
@@ -292,6 +331,13 @@ def _partitioner_for(name: str) -> "CudaPartitioner":
292331 )
293332 constant_methods ["get_min_prefill_chunk" ] = _CUDA_MIN_PREFILL_CHUNK
294333 constant_methods ["use_sampling" ] = sample
334+ if offgraph_manifest is not None :
335+ # The manifest is a compile spec (the lowering pass needs geometry at
336+ # partition time). The runtime instead reads the neutral cache-geometry
337+ # constant methods, the same ones the MLX off-graph path publishes.
338+ # Sizing already travels as get_max_seq_len (context) and
339+ # get_max_prefill_chunk (largest step); only the geometry is new.
340+ constant_methods .update (offgraph_geometry )
295341
296342 print (
297343 f"Lowering { len (programs )} methods to ExecuTorch (CUDA): "
@@ -570,6 +616,11 @@ def main() -> None:
570616 action = "store_true" ,
571617 help = "Use TurboQuant TQ4 KV cache on global (NoPE) layers (CUDA)." ,
572618 )
619+ parser .add_argument (
620+ "--use-offgraph-kv-cache" ,
621+ action = "store_true" ,
622+ help = "Allocate CUDA KV cache at runtime instead of storing it in the PTE/PTD." ,
623+ )
573624 parser .add_argument (
574625 "--activation-dtype" ,
575626 default = None ,
@@ -623,6 +674,10 @@ def main() -> None:
623674 parser .error ("--activation-dtype is only supported with --backend mlx." )
624675 if args .backend != "mlx" and args .max_prefill_chunk != 512 :
625676 parser .error ("--max-prefill-chunk is only supported with --backend mlx." )
677+ if args .use_offgraph_kv_cache and args .backend != "cuda" :
678+ parser .error ("--use-offgraph-kv-cache requires --backend cuda." )
679+ if args .use_offgraph_kv_cache and args .turboquant :
680+ parser .error ("--use-offgraph-kv-cache cannot be combined with --turboquant." )
626681 if args .gguf :
627682 from executorch .examples .models .muse_glimmer .loaders .checkpoint_loader import (
628683 load_gguf_model ,
@@ -679,6 +734,7 @@ def main() -> None:
679734 backend = args .backend ,
680735 sample = not args .logits ,
681736 use_turboquant = args .turboquant ,
737+ use_offgraph_kv_cache = args .use_offgraph_kv_cache ,
682738 activation_dtype = activation_dtype ,
683739 max_prefill_chunk = args .max_prefill_chunk ,
684740 vision_model = vision_model ,
0 commit comments