Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/gpu/modal_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,12 @@ def rebuild_int8_head32(model_id: str, limit: int = 0) -> dict:
checkpoint = Path(spec["volume"]) / spec["checkpoint"]
test_path = Path(spec["test_volume"]) / spec["test_data"]

from imf.export import head_matmul_names, load_byte_seq2seq, quantize_int8
from imf.export import (
head_matmul_names,
load_byte_seq2seq,
quantize_int8,
refresh_member_shas,
)
from imf.parity import (
reference_decode,
run_margin_analysis,
Expand Down Expand Up @@ -478,6 +483,10 @@ def rebuild_int8_head32(model_id: str, limit: int = 0) -> dict:
else:
dst.writestr(name, src.read(name))

# re-quantized graphs replaced the members; the internal sha table
# must be refreshed or strict validation (write_parity) rejects the zip
refresh_member_shas(new_zip)

reference = reference_decode(
model,
[s for s, _ in pairs],
Expand Down
33 changes: 33 additions & 0 deletions src/imf/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,39 @@ def convert_fp16(model):
return copy.deepcopy(model).half()


def refresh_member_shas(zip_path: Path | str) -> None:
"""Recompute the metadata.yaml sha256 table after members were
replaced inside a zip (the head32 re-quantization rewrites the
encoder/decoder graphs in place); the member set is unchanged, so
only the digests drift — until they are refreshed, strict zip
validation rejects the artifact (write_parity gates on it)."""

import hashlib
import os
import tempfile
import zipfile

import yaml

zip_path = Path(zip_path)
with zipfile.ZipFile(zip_path) as zf:
meta = yaml.safe_load(zf.read("metadata.yaml"))
for name in list(meta["sha256"]):
meta["sha256"][name] = hashlib.sha256(zf.read(name)).hexdigest()

with tempfile.TemporaryDirectory(dir=zip_path.parent) as tmp:
rewritten = Path(tmp) / "rewritten.zip"
with zipfile.ZipFile(zip_path) as src, zipfile.ZipFile(
rewritten, "w", zipfile.ZIP_DEFLATED
) as dst:
for member in src.namelist():
if member == "metadata.yaml":
dst.writestr(member, yaml.safe_dump(meta, sort_keys=False))
else:
dst.writestr(member, src.read(member))
os.replace(rewritten, zip_path)


def quantize_int8(
src: Path | str, dst: Path | str, per_channel: bool = False,
nodes_to_exclude: list[str] | None = None,
Expand Down
34 changes: 34 additions & 0 deletions tests/test_imf_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,37 @@ def test_int8_keeps_head_matmul_in_fp32(zips: dict[str, Path]) -> None:
assert head in nodes, (graph_name, head)
# converted heads become MatMulInteger with the same name
assert nodes[head].op_type == "MatMul", (graph_name, head)


def test_refresh_member_shas_after_member_replacement(zips: dict[str, Path]) -> None:
"""The head32 rebuild replaces graphs inside an existing zip; the
stale sha table must make validation fail, and the refresh must
restore it without touching any other member."""
import zipfile

from imf.export import refresh_member_shas

path = zips["fixture-1.0-int8.zip"]
with zipfile.ZipFile(path) as src_zf:
members = {n: src_zf.read(n) for n in src_zf.namelist()}
import onnx

graph = onnx.load_from_string(members["encoder.onnx"])
graph.graph.name += "-tampered"
tampered = graph.SerializeToString()
members["encoder.onnx"] = tampered
tmp = path.with_suffix(".rewritten")
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as dst:
for n, data in members.items():
dst.writestr(n, data)
tmp.replace(path)

result = validate_zip(path)
assert not result.ok # stale table rejects the replaced member

refresh_member_shas(path)
result = validate_zip(path)
assert result.ok, result.errors
with zipfile.ZipFile(path) as after_zf:
assert after_zf.read("encoder.onnx") == tampered
assert after_zf.read("decoder.onnx") == members["decoder.onnx"]
Loading