Skip to content

Commit 6d6094d

Browse files
physicsrobclaude
andcommitted
Tests for the compile_* API simplification
- test_headless_onnx.py: _export returns the OnnxArtifact (call sites read .path); new tests pin artifact fields against export params, debug_path=None under debug_sidecar=False, artifact.load() parity with direct OnnxHeadlessModule construction, the optimize kwarg, and load_onnx dispatch + doctored-format / missing-sidecar errors. - test_module.py: token artifact fields (vocab_size is the embedding TABLE row count — the logits width — not the tokenizer list length), .load() -> OnnxTokenModule + generate round-trip, extra_metadata nested under 'extra' (surfaced via OnnxTokenModule.metadata and OnnxDebugSession.metadata['extra']; top-level keys unchanged, no 'extra' key when omitted). - test_onnx_debug_session.py: artifact.debug_session smoke — handle-built session probes clean and output-matches a directly built one. Fingerprint pins untouched. - test_io_api.py: assume_zero_init=True output-parity on a small graph. Verified: full suite green on Modal (3 shards); ONNX-dependent files green locally; e2e smoke (compile_to_onnx -> artifact.load().generate -> artifact.debug_session + probe_compiled clean; root onnx_repl.py answers 4+5 -> 9). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3d94208 commit 6d6094d

4 files changed

Lines changed: 283 additions & 16 deletions

File tree

tests/compile/forward/test_headless_onnx.py

Lines changed: 126 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ def _build_sample_graph():
8585

8686

8787
def _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

102102
def _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)

tests/compile/forward/test_io_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,27 @@ def test_anonymous_input_with_io():
302302
inp = torch.tensor([[1.0, 2.0, 3.0, 4.0]])
303303
out = module(inp)
304304
assert torch.allclose(out, inp, atol=0.1)
305+
306+
307+
# ---------------------------------------------------------------------------
308+
# Schedule-parity kwargs
309+
# ---------------------------------------------------------------------------
310+
311+
312+
def test_assume_zero_init_kwarg_output_parity():
313+
"""assume_zero_init=True threads to forward_compile and preserves
314+
outputs on a graph whose correctness doesn't depend on dirty-column
315+
cancels (the in-process runtime builds the stream from zeros)."""
316+
317+
def build():
318+
pos = create_pos_encoding()
319+
x = create_input("x", 4)
320+
return multiply_const(x, 2.0), pos
321+
322+
y1, pos1 = build()
323+
default = compile_headless(y1, pos1, d=64, verbose=False)
324+
y2, pos2 = build()
325+
azi = compile_headless(y2, pos2, d=64, verbose=False, assume_zero_init=True)
326+
327+
inp = torch.tensor([[1.0, 2.0, 3.0, 4.0]])
328+
assert torch.allclose(default(inp), azi(inp), atol=0.1)

tests/compile/forward/test_module.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,106 @@ def test_token_onnx_sidecar_schema_and_metadata():
299299
assert len(model.per_layer_n_heads) == model.n_layers
300300
assert all(nh <= D // D_HEAD for nh in model.per_layer_n_heads)
301301
assert model.d_head == D_HEAD
302+
303+
304+
# ---------------------------------------------------------------------------
305+
# Test 6: OnnxArtifact return handle + extra_metadata (token exporter)
306+
# ---------------------------------------------------------------------------
307+
308+
309+
def test_token_onnx_artifact_fields_load_and_generate():
310+
from torchwright.compiler.export import debug_meta_path_for
311+
from torchwright.compiler.onnx_load import OnnxTokenModule
312+
313+
output_node, pos_encoding, embedding = _build_1digit()
314+
315+
with tempfile.TemporaryDirectory() as tmpdir:
316+
onnx_path = os.path.join(tmpdir, "adder.onnx")
317+
artifact = compile_to_onnx(
318+
output_node,
319+
pos_encoding,
320+
embedding,
321+
onnx_path,
322+
d=D,
323+
d_head=D_HEAD,
324+
verbose=False,
325+
)
326+
327+
assert artifact.kind == "token"
328+
assert artifact.path == onnx_path
329+
assert artifact.meta_path == meta_path_for(onnx_path)
330+
assert artifact.debug_path == debug_meta_path_for(onnx_path)
331+
assert artifact.d == D
332+
assert artifact.d_head == D_HEAD
333+
assert artifact.cache_stride == 512 # max_seq_len default
334+
assert artifact.cache_window is None
335+
# vocab_size is the embedding TABLE's row count (the logits
336+
# width) — the table is padded past the tokenizer's vocab list.
337+
assert artifact.vocab_size == embedding.table.shape[0]
338+
assert artifact.d_embed == embedding.table.shape[1]
339+
assert artifact.n_layers > 0
340+
assert artifact.per_layer_n_heads == tuple(
341+
OnnxTokenModule(onnx_path).per_layer_n_heads
342+
)
343+
344+
model = artifact.load()
345+
assert isinstance(model, OnnxTokenModule)
346+
assert "".join(model.generate("2+3\n")) == "5"
347+
348+
349+
def test_token_onnx_extra_metadata_roundtrip():
350+
from torchwright.compiler.onnx_load import load_onnx
351+
from torchwright.debug.onnx_debug import OnnxDebugSession
352+
353+
output_node, pos_encoding, embedding = _build_1digit()
354+
extra = {"rows_per_patch": 7, "note": "hello"}
355+
356+
with tempfile.TemporaryDirectory() as tmpdir:
357+
onnx_path = os.path.join(tmpdir, "adder.onnx")
358+
compile_to_onnx(
359+
output_node,
360+
pos_encoding,
361+
embedding,
362+
onnx_path,
363+
d=D,
364+
d_head=D_HEAD,
365+
verbose=False,
366+
extra_metadata=extra,
367+
)
368+
369+
# Nested under "extra"; top-level keys unchanged.
370+
with open(meta_path_for(onnx_path)) as f:
371+
meta = json.load(f)
372+
assert meta["extra"] == extra
373+
assert meta["format"] == TOKEN_META_FORMAT
374+
assert meta["vocab"] == embedding.tokenizer.vocab
375+
assert meta["cache_stride"] == 512
376+
377+
# Surfaced by the loader...
378+
model = load_onnx(onnx_path)
379+
assert model.metadata == extra
380+
381+
# ...and by the debug session (full sidecar dict there).
382+
out2, pos2, _emb2 = _build_1digit()
383+
sess = OnnxDebugSession(onnx_path, out2, pos2)
384+
assert sess.metadata["extra"] == extra
385+
386+
387+
def test_token_onnx_meta_has_no_extra_key_when_omitted():
388+
output_node, pos_encoding, embedding = _build_1digit()
389+
390+
with tempfile.TemporaryDirectory() as tmpdir:
391+
onnx_path = os.path.join(tmpdir, "adder.onnx")
392+
compile_to_onnx(
393+
output_node,
394+
pos_encoding,
395+
embedding,
396+
onnx_path,
397+
d=D,
398+
d_head=D_HEAD,
399+
verbose=False,
400+
)
401+
with open(meta_path_for(onnx_path)) as f:
402+
meta = json.load(f)
403+
assert "extra" not in meta
404+
assert set(meta) == {"format", "vocab", "cache_stride"}

0 commit comments

Comments
 (0)