@@ -85,8 +85,9 @@ def _build_sample_graph():
8585
8686
8787def _export (output_node , pos_encoding , tmpdir , name = "model.onnx" , trim_heads = True ):
88+ """Export and return the OnnxArtifact (model path at ``.path``)."""
8889 onnx_path = os .path .join (tmpdir , name )
89- compile_headless_to_onnx (
90+ return compile_headless_to_onnx (
9091 output_node ,
9192 pos_encoding ,
9293 onnx_path ,
@@ -96,7 +97,6 @@ def _export(output_node, pos_encoding, tmpdir, name="model.onnx", trim_heads=Tru
9697 verbose = False ,
9798 trim_heads = trim_heads ,
9899 )
99- return onnx_path
100100
101101
102102def _l_w1_d_hidden (onnx_path ):
@@ -149,7 +149,7 @@ def test_headless_onnx_prefill_matches_compute():
149149 )
150150
151151 with tempfile .TemporaryDirectory () as tmpdir :
152- onnx_path = _export (out , pos , tmpdir )
152+ onnx_path = _export (out , pos , tmpdir ). path
153153 session = onnxruntime .InferenceSession (onnx_path )
154154 n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
155155
@@ -178,7 +178,7 @@ def test_headless_onnx_chunked_decode_matches_full_prefill():
178178 inputs_np = torch .cat ([a_vals , b_vals ], dim = 1 ).numpy ().astype (np .float32 )
179179
180180 with tempfile .TemporaryDirectory () as tmpdir :
181- onnx_path = _export (out , pos , tmpdir )
181+ onnx_path = _export (out , pos , tmpdir ). path
182182 session = onnxruntime .InferenceSession (onnx_path )
183183 n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
184184 out_names = ["outputs" ]
@@ -213,7 +213,7 @@ def test_headless_onnx_decode_step_matches_full_prefill():
213213 inputs_np = torch .cat ([a_vals , b_vals ], dim = 1 ).numpy ().astype (np .float32 )
214214
215215 with tempfile .TemporaryDirectory () as tmpdir :
216- onnx_path = _export (out , pos , tmpdir )
216+ onnx_path = _export (out , pos , tmpdir ). path
217217 session = onnxruntime .InferenceSession (onnx_path )
218218 n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
219219 out_names = ["outputs" ]
@@ -259,7 +259,7 @@ def test_headless_onnx_static_tail_is_inert():
259259 n = 4 # rows committed to the cache before the decode step
260260
261261 with tempfile .TemporaryDirectory () as tmpdir :
262- onnx_path = _export (out , pos , tmpdir )
262+ onnx_path = _export (out , pos , tmpdir ). path
263263 session = onnxruntime .InferenceSession (onnx_path )
264264 n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
265265 out_names = ["outputs" ]
@@ -319,7 +319,7 @@ def test_headless_onnx_prefix_window_binding():
319319 n = 4 # committed rows before the decode step
320320
321321 with tempfile .TemporaryDirectory () as tmpdir :
322- onnx_path = _export (out , pos , tmpdir )
322+ onnx_path = _export (out , pos , tmpdir ). path
323323 session = onnxruntime .InferenceSession (onnx_path )
324324 n_layers , per_layer_n_heads , d_head , S = _discover_meta (session , onnx_path )
325325 out_names = ["outputs" ]
@@ -381,7 +381,7 @@ def test_onnx_headless_module_step_matches_full_call():
381381 inputs = torch .cat ([a_vals , b_vals ], dim = 1 )
382382
383383 with tempfile .TemporaryDirectory () as tmpdir :
384- onnx_path = _export (out , pos , tmpdir )
384+ onnx_path = _export (out , pos , tmpdir ). path
385385 module = OnnxHeadlessModule (onnx_path )
386386
387387 # Independent call: prefill full sequence, drop cache
@@ -410,7 +410,7 @@ def test_onnx_headless_module_empty_past_shape():
410410
411411 out , pos = _build_sample_graph ()
412412 with tempfile .TemporaryDirectory () as tmpdir :
413- onnx_path = _export (out , pos , tmpdir )
413+ onnx_path = _export (out , pos , tmpdir ). path
414414 module = OnnxHeadlessModule (onnx_path )
415415 past = module .empty_past ()
416416 S = module .cache_stride
@@ -439,7 +439,7 @@ def test_headless_onnx_sidecar_schema():
439439 pos = create_pos_encoding ()
440440
441441 with tempfile .TemporaryDirectory () as tmpdir :
442- onnx_path = _export (out , pos , tmpdir )
442+ onnx_path = _export (out , pos , tmpdir ). path
443443 meta_path = meta_path_for (onnx_path )
444444 with open (meta_path ) as f :
445445 data = json .load (f )
@@ -524,7 +524,7 @@ def test_headless_onnx_trim_heads_shrinks_kv_cache():
524524 out , pos = _build_sample_graph ()
525525 max_heads = D // D_HEAD # 16
526526 with tempfile .TemporaryDirectory () as tmpdir :
527- onnx_path = _export (out , pos , tmpdir , trim_heads = True )
527+ onnx_path = _export (out , pos , tmpdir , trim_heads = True ). path
528528 session = onnxruntime .InferenceSession (onnx_path )
529529 _ , per_layer_n_heads , _ , _ = _discover_meta (session , onnx_path )
530530
@@ -546,7 +546,7 @@ def test_headless_onnx_no_trim_preserves_full_width():
546546 out , pos = _build_sample_graph ()
547547 max_heads = D // D_HEAD # 16
548548 with tempfile .TemporaryDirectory () as tmpdir :
549- onnx_path = _export (out , pos , tmpdir , trim_heads = False )
549+ onnx_path = _export (out , pos , tmpdir , trim_heads = False ). path
550550 session = onnxruntime .InferenceSession (onnx_path )
551551 _ , per_layer_n_heads , _ , _ = _discover_meta (session , onnx_path )
552552
@@ -561,8 +561,8 @@ def test_headless_onnx_trim_shrinks_mlp_slots():
561561 out , pos = _build_sample_graph ()
562562 full_d_hidden = D # d_hidden defaults to d when omitted
563563 with tempfile .TemporaryDirectory () as tmpdir :
564- trim_path = _export (out , pos , tmpdir , name = "trim.onnx" , trim_heads = True )
565- notrim_path = _export (out , pos , tmpdir , name = "notrim.onnx" , trim_heads = False )
564+ trim_path = _export (out , pos , tmpdir , name = "trim.onnx" , trim_heads = True ). path
565+ notrim_path = _export (out , pos , tmpdir , name = "notrim.onnx" , trim_heads = False ). path
566566 trim_widths = _l_w1_d_hidden (trim_path )
567567 notrim_widths = _l_w1_d_hidden (notrim_path )
568568
@@ -588,8 +588,8 @@ def test_headless_onnx_trim_is_numerical_noop():
588588 inputs_np = torch .cat ([a_vals , b_vals ], dim = 1 ).numpy ().astype (np .float32 )
589589
590590 with tempfile .TemporaryDirectory () as tmpdir :
591- trim_path = _export (out , pos , tmpdir , name = "trim.onnx" , trim_heads = True )
592- notrim_path = _export (out , pos , tmpdir , name = "notrim.onnx" , trim_heads = False )
591+ trim_path = _export (out , pos , tmpdir , name = "trim.onnx" , trim_heads = True ). path
592+ notrim_path = _export (out , pos , tmpdir , name = "notrim.onnx" , trim_heads = False ). path
593593
594594 sess_trim = onnxruntime .InferenceSession (trim_path )
595595 sess_notrim = onnxruntime .InferenceSession (notrim_path )
@@ -607,3 +607,113 @@ def test_headless_onnx_trim_is_numerical_noop():
607607 f"trim changed the output: max diff "
608608 f"{ np .abs (out_trim - out_notrim ).max ():.6e} "
609609 )
610+
611+
612+ # ---------------------------------------------------------------------------
613+ # OnnxArtifact return handle
614+ # ---------------------------------------------------------------------------
615+
616+
617+ def test_headless_onnx_artifact_fields_and_load ():
618+ """The exporter's OnnxArtifact matches the export params, and
619+ artifact.load() behaves identically to a directly constructed
620+ OnnxHeadlessModule."""
621+ from torchwright .compiler .export import debug_meta_path_for
622+ from torchwright .compiler .onnx_load import OnnxHeadlessModule
623+
624+ out , pos = _build_sample_graph ()
625+ a_vals = torch .tensor ([[3.0 ], [5.0 ], [- 2.0 ]])
626+ b_vals = torch .tensor ([[4.0 ], [- 1.0 ], [3.0 ]])
627+ inp = torch .cat ([a_vals , b_vals ], dim = 1 )
628+
629+ with tempfile .TemporaryDirectory () as tmpdir :
630+ artifact = _export (out , pos , tmpdir )
631+
632+ assert artifact .kind == "headless"
633+ assert artifact .path == os .path .join (tmpdir , "model.onnx" )
634+ assert artifact .meta_path == meta_path_for (artifact .path )
635+ assert artifact .debug_path == debug_meta_path_for (artifact .path )
636+ assert os .path .exists (artifact .path )
637+ assert os .path .exists (artifact .meta_path )
638+ assert os .path .exists (artifact .debug_path )
639+ assert artifact .d == D
640+ assert artifact .d_head == D_HEAD
641+ assert artifact .cache_stride == 32 # = max_seq_len default in _export
642+ assert artifact .cache_window is None
643+ assert artifact .d_embed is None and artifact .vocab_size is None
644+ assert artifact .n_layers > 0
645+ assert isinstance (artifact .per_layer_n_heads , tuple )
646+ assert len (artifact .per_layer_n_heads ) == artifact .n_layers
647+
648+ loaded = artifact .load ()
649+ assert isinstance (loaded , OnnxHeadlessModule )
650+ direct = OnnxHeadlessModule (artifact .path )
651+ assert torch .allclose (loaded (inp ), direct (inp ), atol = 0 )
652+
653+
654+ def test_headless_onnx_artifact_no_debug_sidecar ():
655+ """debug_sidecar=False -> artifact.debug_path is None, no file written."""
656+ from torchwright .compiler .export import debug_meta_path_for
657+
658+ out , pos = _build_sample_graph ()
659+
660+ with tempfile .TemporaryDirectory () as tmpdir :
661+ onnx_path = os .path .join (tmpdir , "model.onnx" )
662+ artifact = compile_headless_to_onnx (
663+ out ,
664+ pos ,
665+ onnx_path ,
666+ d = D ,
667+ d_head = D_HEAD ,
668+ max_seq_len = 32 ,
669+ verbose = False ,
670+ debug_sidecar = False ,
671+ )
672+ assert artifact .debug_path is None
673+ assert not os .path .exists (debug_meta_path_for (onnx_path ))
674+
675+
676+ def test_headless_onnx_optimize_kwarg_accepted ():
677+ """compile_headless_to_onnx accepts optimize=0 (schedule parity kwarg)."""
678+ out , pos = _build_sample_graph ()
679+
680+ with tempfile .TemporaryDirectory () as tmpdir :
681+ onnx_path = os .path .join (tmpdir , "model.onnx" )
682+ artifact = compile_headless_to_onnx (
683+ out ,
684+ pos ,
685+ onnx_path ,
686+ d = D ,
687+ d_head = D_HEAD ,
688+ max_seq_len = 32 ,
689+ verbose = False ,
690+ optimize = 0 ,
691+ )
692+ assert os .path .exists (artifact .path )
693+
694+
695+ def test_load_onnx_dispatches_headless ():
696+ """load_onnx on a headless export returns an OnnxHeadlessModule;
697+ a doctored sidecar format raises a loud ValueError."""
698+ from torchwright .compiler .onnx_load import OnnxHeadlessModule , load_onnx
699+
700+ out , pos = _build_sample_graph ()
701+
702+ with tempfile .TemporaryDirectory () as tmpdir :
703+ artifact = _export (out , pos , tmpdir )
704+ model = load_onnx (artifact .path )
705+ assert isinstance (model , OnnxHeadlessModule )
706+
707+ # Doctor the format key -> loud error naming both expected values.
708+ with open (artifact .meta_path ) as f :
709+ meta = json .load (f )
710+ meta ["format" ] = "torchwright.bogus.v9"
711+ with open (artifact .meta_path , "w" ) as f :
712+ json .dump (meta , f )
713+ with pytest .raises (ValueError , match = "bogus" ):
714+ load_onnx (artifact .path )
715+
716+ # Missing sidecar -> FileNotFoundError with re-export hint.
717+ os .remove (artifact .meta_path )
718+ with pytest .raises (FileNotFoundError , match = "Re-export" ):
719+ load_onnx (artifact .path )
0 commit comments