2828D_HEAD = 16
2929
3030
31- def _discover_meta (session ):
32- # past_K_i is sequence-major with a STATIC slot count: (S, n_heads, d_head).
31+ def _discover_meta (session , onnx_path ):
32+ # past_K_i is sequence-major (cache_slots, n_heads, d_head) with a
33+ # SYMBOLIC slot dim — the bound prefix length S_eff (stride bucketing).
34+ # The full stride S comes from the sidecar meta.
35+ from torchwright .compiler .onnx_load import discover_cache_stride
36+
3337 inputs = {inp .name : inp for inp in session .get_inputs ()}
3438 n_layers = sum (1 for name in inputs if name .startswith ("past_K_" ))
3539 per_layer_n_heads = [int (inputs [f"past_K_{ i } " ].shape [1 ]) for i in range (n_layers )]
3640 d_head = int (inputs ["past_K_0" ].shape [2 ])
37- cache_stride = inputs ["past_K_0" ].shape [0 ]
38- assert isinstance (cache_stride , int ), (
39- f"past_K_0 first dim must be static , got { cache_stride !r} "
41+ slot_dim = inputs ["past_K_0" ].shape [0 ]
42+ assert not isinstance (slot_dim , int ), (
43+ f"past_K_0 first dim must be the symbolic cache_slots , got { slot_dim !r} "
4044 )
45+ with open (meta_path_for (onnx_path )) as f :
46+ sidecar = json .load (f )
47+ cache_stride = discover_cache_stride (inputs , sidecar .get ("cache_stride" ), onnx_path )
4148 return n_layers , per_layer_n_heads , d_head , cache_stride
4249
4350
@@ -144,7 +151,7 @@ def test_headless_onnx_prefill_matches_compute():
144151 with tempfile .TemporaryDirectory () as tmpdir :
145152 onnx_path = _export (out , pos , tmpdir )
146153 session = onnxruntime .InferenceSession (onnx_path )
147- n_layers , per_layer_n_heads , d_head , S = _discover_meta (session )
154+ n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
148155
149156 inputs_np = torch .cat ([a_vals , b_vals ], dim = 1 ).numpy ().astype (np .float32 )
150157 past_k , past_v = _zero_past (per_layer_n_heads , d_head , S )
@@ -173,7 +180,7 @@ def test_headless_onnx_chunked_decode_matches_full_prefill():
173180 with tempfile .TemporaryDirectory () as tmpdir :
174181 onnx_path = _export (out , pos , tmpdir )
175182 session = onnxruntime .InferenceSession (onnx_path )
176- n_layers , per_layer_n_heads , d_head , S = _discover_meta (session )
183+ n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
177184 out_names = ["outputs" ]
178185 for i in range (n_layers ):
179186 out_names += [f"delta_K_{ i } " , f"delta_V_{ i } " ]
@@ -208,7 +215,7 @@ def test_headless_onnx_decode_step_matches_full_prefill():
208215 with tempfile .TemporaryDirectory () as tmpdir :
209216 onnx_path = _export (out , pos , tmpdir )
210217 session = onnxruntime .InferenceSession (onnx_path )
211- n_layers , per_layer_n_heads , d_head , S = _discover_meta (session )
218+ n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
212219 out_names = ["outputs" ]
213220 for i in range (n_layers ):
214221 out_names += [f"delta_K_{ i } " , f"delta_V_{ i } " ]
@@ -254,7 +261,7 @@ def test_headless_onnx_static_tail_is_inert():
254261 with tempfile .TemporaryDirectory () as tmpdir :
255262 onnx_path = _export (out , pos , tmpdir )
256263 session = onnxruntime .InferenceSession (onnx_path )
257- n_layers , per_layer_n_heads , d_head , S = _discover_meta (session )
264+ n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
258265 out_names = ["outputs" ]
259266 for i in range (n_layers ):
260267 out_names += [f"delta_K_{ i } " , f"delta_V_{ i } " ]
@@ -294,6 +301,72 @@ def decode(pk, pv):
294301 )
295302
296303
304+ # ---------------------------------------------------------------------------
305+ # Test 2c: prefix-window (stride-bucket) bindings are equivalent.
306+ # The symbolic cache_slots dim lets a feeder bind any prefix cache[:S_eff]
307+ # with base + n_new <= S_eff <= S; the in-graph mask derives its width from
308+ # Shape(past_K_0), so every covering window must produce the same outputs.
309+ # On CPU with the same n_new the kernels are identical, so the comparison
310+ # is bit-level, not just allclose.
311+ # ---------------------------------------------------------------------------
312+
313+
314+ def test_headless_onnx_prefix_window_binding ():
315+ out , pos = _build_sample_graph ()
316+ a_vals = torch .tensor ([[3.0 ], [5.0 ], [- 2.0 ], [0.0 ], [4.0 ]])
317+ b_vals = torch .tensor ([[4.0 ], [- 1.0 ], [3.0 ], [7.0 ], [2.0 ]])
318+ inputs_np = torch .cat ([a_vals , b_vals ], dim = 1 ).numpy ().astype (np .float32 )
319+ n = 4 # committed rows before the decode step
320+
321+ with tempfile .TemporaryDirectory () as tmpdir :
322+ onnx_path = _export (out , pos , tmpdir )
323+ session = onnxruntime .InferenceSession (onnx_path )
324+ n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
325+ out_names = ["outputs" ]
326+ for i in range (n_layers ):
327+ out_names += [f"delta_K_{ i } " , f"delta_V_{ i } " ]
328+
329+ # Prefill n rows into the full-S cache.
330+ past_k , past_v = _zero_past (per_layer_n_heads , d_head , S )
331+ results = session .run (out_names , _feeds (inputs_np [:n ], past_k , past_v , 0 ))
332+ _write_deltas (past_k , past_v , results , 0 , n )
333+
334+ def decode_window (s_eff ):
335+ pk = [k [:s_eff ] for k in past_k ] # contiguous prefix views
336+ pv = [v [:s_eff ] for v in past_v ]
337+ return session .run (
338+ out_names , _feeds (inputs_np [n : n + 1 ], pk , pv , n )
339+ )
340+
341+ # Full-S binding is the reference; every covering prefix window
342+ # (smallest legal = base + n_new = n + 1) must match bit-for-bit.
343+ ref = decode_window (S )
344+ for s_eff in (n + 1 , n + 3 , S - 1 ):
345+ got = decode_window (s_eff )
346+ for r , g , name in zip (ref , got , out_names ):
347+ assert np .array_equal (r , g ), (
348+ f"S_eff={ s_eff } : { name } differs from the full-S binding "
349+ f"(max diff { np .abs (r - g ).max ():.6e} )"
350+ )
351+
352+ # Prefill itself also rides a window: prefill at the smallest
353+ # covering bucket equals prefill at full S.
354+ pk_a , pv_a = _zero_past (per_layer_n_heads , d_head , S )
355+ full = session .run (out_names , _feeds (inputs_np , pk_a , pv_a , 0 ))
356+ pk_b , pv_b = _zero_past (per_layer_n_heads , d_head , S )
357+ win = session .run (
358+ out_names ,
359+ _feeds (
360+ inputs_np ,
361+ [k [: len (inputs_np )] for k in pk_b ],
362+ [v [: len (inputs_np )] for v in pv_b ],
363+ 0 ,
364+ ),
365+ )
366+ for r , g , name in zip (full , win , out_names ):
367+ assert np .array_equal (r , g ), f"prefill window: { name } differs"
368+
369+
297370# ---------------------------------------------------------------------------
298371# Test 3: OnnxHeadlessModule.step API threads the cache correctly
299372# ---------------------------------------------------------------------------
@@ -373,6 +446,9 @@ def test_headless_onnx_sidecar_schema():
373446
374447 assert data ["format" ] == HEADLESS_META_FORMAT
375448 assert data ["input_names" ] == ["alpha" , "middle" , "zebra" ]
449+ # The full stride S travels in the sidecar (the symbolic cache_slots
450+ # input dim is not readable as an int).
451+ assert data ["cache_stride" ] == 32 # _export passes max_seq_len=32
376452 # The sidecar should not carry any "cached" discriminator — there's
377453 # only one protocol now.
378454 assert "cached" not in data
@@ -450,7 +526,7 @@ def test_headless_onnx_trim_heads_shrinks_kv_cache():
450526 with tempfile .TemporaryDirectory () as tmpdir :
451527 onnx_path = _export (out , pos , tmpdir , trim_heads = True )
452528 session = onnxruntime .InferenceSession (onnx_path )
453- _ , per_layer_n_heads , _ , _ = _discover_meta (session )
529+ _ , per_layer_n_heads , _ , _ = _discover_meta (session , onnx_path )
454530
455531 assert per_layer_n_heads , "no layers discovered"
456532 assert all (1 <= nh <= max_heads for nh in per_layer_n_heads )
@@ -472,7 +548,7 @@ def test_headless_onnx_no_trim_preserves_full_width():
472548 with tempfile .TemporaryDirectory () as tmpdir :
473549 onnx_path = _export (out , pos , tmpdir , trim_heads = False )
474550 session = onnxruntime .InferenceSession (onnx_path )
475- _ , per_layer_n_heads , _ , _ = _discover_meta (session )
551+ _ , per_layer_n_heads , _ , _ = _discover_meta (session , onnx_path )
476552
477553 assert per_layer_n_heads , "no layers discovered"
478554 assert all (nh == max_heads for nh in per_layer_n_heads ), (
@@ -518,8 +594,8 @@ def test_headless_onnx_trim_is_numerical_noop():
518594 sess_trim = onnxruntime .InferenceSession (trim_path )
519595 sess_notrim = onnxruntime .InferenceSession (notrim_path )
520596
521- _ , heads_trim , d_head , S_trim = _discover_meta (sess_trim )
522- _ , heads_notrim , _ , S_notrim = _discover_meta (sess_notrim )
597+ _ , heads_trim , d_head , S_trim = _discover_meta (sess_trim , trim_path )
598+ _ , heads_notrim , _ , S_notrim = _discover_meta (sess_notrim , notrim_path )
523599
524600 pk , pv = _zero_past (heads_trim , d_head , S_trim )
525601 out_trim = sess_trim .run (["outputs" ], _feeds (inputs_np , pk , pv , 0 ))[0 ]
0 commit comments