@@ -386,6 +386,7 @@ async def _execute_and_compare(
386386 num_calls : int ,
387387 produce_torch_out : Any ,
388388 compare : Any ,
389+ coreai_program : Any = None ,
389390 dump_path : Path | None = None ,
390391) -> None :
391392 """The single place we invoke the Core AI runtime.
@@ -394,28 +395,17 @@ async def _execute_and_compare(
394395 two callbacks let path A (stateful, recompute per call) and path B
395396 (fixed expected output) share this loop without ad-hoc branching.
396397 """
398+ # Stateful tests can't round-trip for now,
399+ # which has no `--input` JSON path for state — so skip the dump entirely.
400+ should_dump = dump_optests_enabled () and dump_path is not None and not state
397401 try :
398402 for call_idx in range (num_calls ):
399- io_numpy = {}
400- if call_idx == 0 and dump_optests_enabled ():
401- assert dump_path is not None
402- for name , arr in state .items ():
403- io_numpy [f"initial_state_{ name } " ] = arr .numpy ()
404-
405403 torch_out = produce_torch_out (call_idx )
406404 rt_outputs = await rt_func (inputs = inputs , state = state )
407405 compare (rt_outputs , torch_out , call_idx )
408406
409- if call_idx == 0 and dump_optests_enabled ():
410- assert dump_path is not None
411- for name , arr in inputs .items ():
412- io_numpy [name ] = arr .numpy ()
413- for name , arr in state .items ():
414- io_numpy [f"final_state_{ name } " ] = arr .numpy ()
415- for name , arr in rt_outputs .items ():
416- io_numpy [name ] = arr .numpy ()
417-
418- np .savez (dump_path / "test_data.npz" , ** io_numpy )
407+ if call_idx == 0 and should_dump :
408+ _dump_optest_artifacts (coreai_program , inputs , rt_outputs , dump_path )
419409
420410 except Exception :
421411 # Wipe bytecode and reference IO if the test failed as
@@ -425,6 +415,43 @@ async def _execute_and_compare(
425415 raise
426416
427417
418+ def _add_npz_entry (io_numpy : dict [str , np .ndarray ], key : str , arr : np .ndarray ) -> None :
419+ """Add an array to the npz dict, emitting a bf16 dtype override if needed.
420+
421+ NumPy has no native bf16, so coreai_torch surfaces it as void16 (``|V2``).
422+ run-all-tests.py's process_numpy_file consults ``_dtype_<key>`` overrides to
423+ recover the MPS-side type — without it, void16 raises in numpy_to_mps_type.
424+ """
425+ io_numpy [key ] = arr
426+ if arr .dtype .str == "|V2" :
427+ io_numpy [f"_dtype_{ key } " ] = np .array ("bf16" )
428+
429+
430+ def _dump_optest_artifacts (
431+ coreai_program : Any ,
432+ inputs : dict [str , NDArray ],
433+ rt_outputs : dict [str , NDArray ],
434+ dump_path : Path ,
435+ ) -> None :
436+ """Write a `<testname>.aimodel` + `<testname>_test_data.npz` pair.
437+
438+ Format: aimodel prefix == npz prefix == dump_path
439+ leaf name; npz holds an ``op_name`` scalar plus ``input_<n>`` /
440+ ``output_<n>`` keys. ``coreai-run`` consumes ``.aimodel`` directly (its
441+ ``main.mlirb`` is bitwise identical to what ``_save_bytecode`` used to
442+ produce, but ``save_asset`` is the public API).
443+ """
444+ testname = dump_path .name
445+ coreai_program .save_asset (dump_path / f"{ testname } .aimodel" )
446+
447+ io_numpy : dict [str , np .ndarray ] = {"op_name" : np .array ("main" )}
448+ for name , arr in inputs .items ():
449+ _add_npz_entry (io_numpy , f"input_{ name } " , arr .numpy ())
450+ for name , arr in rt_outputs .items ():
451+ _add_npz_entry (io_numpy , f"output_{ name } " , arr .numpy ())
452+ np .savez (dump_path / f"{ testname } _test_data.npz" , ** io_numpy )
453+
454+
428455async def _run_with_model (
429456 model : torch .nn .Module ,
430457 rt_func : Any ,
@@ -437,6 +464,7 @@ async def _run_with_model(
437464 rtol : float ,
438465 atol : float ,
439466 metal_inputs : bool = False ,
467+ coreai_program : Any = None ,
440468 dump_path : Path | None = None ,
441469) -> None :
442470 """Path A: stateful, multi-call, name-based matching."""
@@ -478,6 +506,7 @@ def compare(
478506 num_calls = num_calls ,
479507 produce_torch_out = produce_torch_out ,
480508 compare = compare ,
509+ coreai_program = coreai_program ,
481510 dump_path = dump_path ,
482511 )
483512
@@ -490,6 +519,7 @@ async def _run_with_program(
490519 rtol : float ,
491520 atol : float ,
492521 metal_inputs : bool = False ,
522+ coreai_program : Any = None ,
493523 dump_path : Path | None = None ,
494524) -> None :
495525 """Path B: pre-converted program, single call, sorted-key matching."""
@@ -519,6 +549,7 @@ def compare(
519549 num_calls = 1 ,
520550 produce_torch_out = produce_torch_out ,
521551 compare = compare ,
552+ coreai_program = coreai_program ,
522553 dump_path = dump_path ,
523554 )
524555
@@ -592,9 +623,6 @@ async def validate_numerical_output(**kwargs: Any) -> None:
592623 if dump_optests_enabled ():
593624 dump_path = _optest_dump_path (get_current_test_id ())
594625 dump_path .mkdir (parents = True , exist_ok = True )
595- model_path = dump_path / "main.AICode.bc"
596- model_path .unlink (missing_ok = True )
597- coreai_program ._save_bytecode (model_path )
598626
599627 with TemporaryDirectory () as temp_directory :
600628 aimodel_path = Path (temp_directory ) / "model.aimodel"
@@ -616,6 +644,7 @@ async def validate_numerical_output(**kwargs: Any) -> None:
616644 rtol = rtol ,
617645 atol = atol ,
618646 metal_inputs = metal_inputs ,
647+ coreai_program = coreai_program ,
619648 dump_path = dump_path ,
620649 )
621650 else :
@@ -626,6 +655,7 @@ async def validate_numerical_output(**kwargs: Any) -> None:
626655 rtol = rtol ,
627656 atol = atol ,
628657 metal_inputs = metal_inputs ,
658+ coreai_program = coreai_program ,
629659 dump_path = dump_path ,
630660 )
631661
0 commit comments