From dcf61ba2bd6d301f4af267b8aa628e2ace5d8bf0 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 3 Sep 2026 17:29:55 +0200 Subject: [PATCH] fix(export): parity/margins functions accept list or string precisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parity/margins entrypoints pass precisions.split(",") — a list — into the remote functions, which then called .split again and crashed with AttributeError. Every entrypoint invocation has been broken since the loops learned to split; only direct ::parity_model CLI calls worked. normalize_precisions accepts both forms. --- src/gpu/modal_export.py | 21 +++++++++++++++------ tests/test_parity_stage_skip.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/gpu/modal_export.py b/src/gpu/modal_export.py index bec6117..f3e6a1d 100644 --- a/src/gpu/modal_export.py +++ b/src/gpu/modal_export.py @@ -191,15 +191,26 @@ app = modal.App("interscript-ml-export", image=IMAGE) +def normalize_precisions(precisions: "str | list[str]") -> list[str]: + """Accept both invocation forms: the parity/margins entrypoints pass + a pre-split list, direct ::parity_model-style CLI calls pass a + comma string.""" + if isinstance(precisions, str): + precisions = precisions.split(",") + return [q.strip() for q in precisions if q.strip()] + + def pending_precisions( - out_dir: Path, mid: str, precisions: list[str] + out_dir: Path, mid: str, precisions: "str | list[str]" ) -> list[str]: """Precision stages still to run: the margin report is the last artifact a stage writes, so its presence means the stage (parity block in the zip included) completed durably. Preemption restarts resume at the next stage instead of redoing hours of decode.""" return [ - p for p in precisions if not (out_dir / f"{mid}-margins-{p}.json").exists() + p + for p in normalize_precisions(precisions) + if not (out_dir / f"{mid}-margins-{p}.json").exists() ] @@ -336,9 +347,7 @@ def stage(event: str) -> None: meta_path = Path("/root/interscript-ml", spec["metadata"]) mid = re.search(r"^id:\s*(\S+)", meta_path.read_text(encoding="utf-8"), re.M).group(1) reports: dict[str, str] = {} - todo = pending_precisions( - out_dir, mid, [q.strip() for q in precisions.split(",") if q.strip()] - ) + todo = pending_precisions(out_dir, mid, precisions) for precision in todo: zip_path = out_dir / f"{mid}-{precision}.zip" stage(f"onnx decode {precision}") @@ -406,7 +415,7 @@ def margin_model( meta_path = Path("/root/interscript-ml", spec["metadata"]) mid = re.search(r"^id:\s*(\S+)", meta_path.read_text(encoding="utf-8"), re.M).group(1) reports: dict[str, str] = {} - for precision in [p.strip() for p in precisions.split(",") if p.strip()]: + for precision in normalize_precisions(precisions): zip_path = out_dir / f"{mid}-{precision}.zip" if not zip_path.exists(): reports[precision] = "zip not exported (skipped)" diff --git a/tests/test_parity_stage_skip.py b/tests/test_parity_stage_skip.py index 13fb229..29f7c49 100644 --- a/tests/test_parity_stage_skip.py +++ b/tests/test_parity_stage_skip.py @@ -33,3 +33,17 @@ def test_zip_without_margin_report_still_runs(tmp_path: Path) -> None: (tmp_path / "ara-diac-small-2.1-int8.zip").write_text("partial") got = pending_precisions(tmp_path, "ara-diac-small-2.1", ["int8"]) assert got == ["int8"] + + +def test_list_input_from_entrypoint(tmp_path: Path) -> None: + # the parity/margins entrypoints pass precisions.split(",") — a + # list — into the remote functions; the functions must accept both + # forms (direct ::parity_model CLI invocation passes a string) + (tmp_path / "ara-diac-small-2.1-margins-fp32.json").write_text("{}") + got = pending_precisions(tmp_path, "ara-diac-small-2.1", ["fp32", "int8"]) + assert got == ["int8"] + + +def test_string_input_strips_whitespace(tmp_path: Path) -> None: + got = pending_precisions(tmp_path, "m", "fp32, int8") + assert got == ["fp32", "int8"]