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
2 changes: 1 addition & 1 deletion docs/imf-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ model.zip
| `license` | str | non-empty (strict gate) |
| `trained_from` | str | repo + run/checkpoint id |
| `metrics` | list | `{name, value, protocol, source}`; `source` must be a `RESULTS.md#anchor` (strict gate) |
| `parity` | map? | `{samples, cer_delta}`; strict gate: samples >= 500, cer_delta <= 0.2pp fp32 / 1.0pp fp16 / 2.0pp int8 |
| `parity` | map? | `{samples, cer_delta}`; strict gate: samples >= 500, cer_delta <= 0.2pp fp32 / 1.0pp fp16 / 2.0pp int8 / 3.0pp int4 |
| `sha256` | map | every `*.onnx` member -> hex digest; no dangling entries |

The `id` does not encode precision: `khm-latn-1.0-fp16.zip` and
Expand Down
21 changes: 19 additions & 2 deletions src/gpu/modal_distill.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"numpy>=1.26",
# arabic gate harness: Misraj evaluator + SadeedDiac-25 parquet
"pyarabic",
"prettytable",
"pandas",
"pyarrow",
)
Expand Down Expand Up @@ -217,6 +218,15 @@
"val": "hebrew-v4/val.jsonl",
"out": "rababa_hebrew_distill_small/run-001",
},
"heb-diac-small-s46": {
# re-distill the client tier from the s46 teacher (greedy 16.44
# vs s43's 29.0) — the 1.0 student tracked its teacher's greedy
"teacher": "rababa_hebrew/run-s46-phonikud-plus/run-002-gold-ft/best",
"student_init": "google/byt5-small",
"train": "hebrew-v4/train.jsonl",
"val": "hebrew-v4/val.jsonl",
"out": "rababa_hebrew_distill_small/run-002-s46",
},
}

app = modal.App("interscript-ml-distill", image=IMAGE)
Expand Down Expand Up @@ -1067,11 +1077,18 @@ def evaluate_der(spec_id: str, window: int = 1400, limit: int = 0) -> dict:
}
teacher_path = (spec["teacher"] if spec.get("teacher_is_hub")
else str(Path(vol_map[spec.get("teacher_volume", "rababa")]) / spec["teacher"]))
student_path = Path(vol_map[spec.get("teacher_volume", "rababa")]) / spec["out"] / "best"
student_path = (
Path(vol_map[spec.get("out_volume", spec.get("teacher_volume", "rababa"))])
/ spec["out"] / "best"
)

tok = AutoTokenizer.from_pretrained("google/byt5-small")
teacher = AutoModelForSeq2SeqLM.from_pretrained(teacher_path).to("cuda").eval()
student = AutoModelForSeq2SeqLM.from_pretrained(str(student_path)).to("cuda").eval()
# custom students carry T5's default max_length=20; windowed inputs
# run to 1400 bytes, clamping max_new_tokens to zero
for m in (teacher, student):
m.generation_config.max_length = 100_000

diac = re.compile("[ً-ٰٟۖ-ۭ]")
table = pq.read_table("/opt/rababa/data/sadeed-diac-25/train.parquet")
Expand Down Expand Up @@ -1412,4 +1429,4 @@ def mk(spec: str = "tha-g2p-tiny-mk", epochs: int = 3) -> None:

@app.local_entrypoint()
def eval_der(spec: str = "ara-diac-small", limit: int = 0) -> None:
print(evaluate_der.remote(spec, limit))
print(evaluate_der.remote(spec, limit=limit))
6 changes: 5 additions & 1 deletion src/gpu/modal_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

from pathlib import Path

import re

import modal

REPO_ROOT = Path(__file__).resolve().parent.parent.parent
Expand Down Expand Up @@ -248,9 +250,11 @@ def parity_model(model_id: str, precisions: list[str], limit: int = 0) -> dict[s
reference = reference_decode(model, [src for src, _ in pairs], max_len=128)

out_dir = Path("/outputs/imf") / model_id
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 precisions:
zip_path = out_dir / f"{model_id}-1.0-{precision}.zip"
zip_path = out_dir / f"{mid}-{precision}.zip"
report = run_parity(model, zip_path, pairs, max_len=128, reference=reference)
reports[precision] = (
f"samples={report.samples} cer_ref={report.cer_reference}pp "
Expand Down
24 changes: 24 additions & 0 deletions src/imf/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,28 @@ def quantize_int8(src: Path | str, dst: Path | str) -> Path:
return Path(dst)


def quantize_int4(src: Path | str, dst: Path | str, block_size: int = 64) -> Path:
"""fp32 -> 4-bit blockwise MatMul (MatMulNBits, com.microsoft domain).

Halves int8 again at some quality cost; meant for the browser/edge
client tier. NOTE: old runtimes (the Ruby gem's bundled ORT) cannot
execute MatMulNBits — int4 zips are client-crystal territory and
their loaders should fail loudly rather than silently fall back.
"""
import onnx
from onnxruntime.quantization.matmul_nbits_quantizer import (
MatMulNBitsQuantizer,
)

model = onnx.load(str(src))
quant = MatMulNBitsQuantizer(
model=model, block_size=block_size, is_symmetric=True
)
quant.process()
quant.model.save_model_to_file(str(dst))
return Path(dst)


def onnx_greedy_plain(encoder_sess, decoder_sess, text: str, max_len: int = 256) -> list[int]:
"""Greedy decode over ONNX sessions (plain decoder). Self-check helper.

Expand Down Expand Up @@ -380,6 +402,8 @@ def export_zips(
dst.write_bytes(src.read_bytes())
elif precision == "int8":
quantize_int8(graphs[name], dst)
elif precision == "int4":
quantize_int4(graphs[name], dst)
else:
raise ValueError(f"unknown precision {precision!r}")
meta = replace(metadata, precision=precision)
Expand Down
2 changes: 1 addition & 1 deletion src/imf/parity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""WO03 parity gate: ONNX greedy vs the torch reference, precision-aware
CER-delta limits (0.2pp fp32, 1.0pp fp16, 2.0pp int8).
CER-delta limits (0.2pp fp32, 1.0pp fp16, 2.0pp int8, 3.0pp int4).

The reference is the transformers decoder loop itself (the exact math the
export wraps) rather than ``model.generate`` — generate's behavior is
Expand Down
4 changes: 2 additions & 2 deletions src/imf/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

TASKS = frozenset({"g2p", "diacritization", "translit"})
DECODERS = frozenset({"plain", "kv"})
PRECISIONS = frozenset({"fp32", "fp16", "int8"})
PRECISIONS = frozenset({"fp32", "fp16", "int8", "int4"})

# The Ruby onnxruntime gem bundles an old ORT that cannot load opset > 14.
# Opset is pinned to 14 and validated against the actual graphs on load.
Expand Down Expand Up @@ -71,7 +71,7 @@ class Parity:
# Quantization widens the torch-vs-ONNX gap: measured deltas on khm
# were ~0.43pp (fp16) and ~0.84pp (int8) against the 0.2pp fp32 bar,
# so the gate is keyed on the declared precision.
MAX_CER_DELTA_BY_PRECISION = {"fp32": 0.2, "fp16": 1.0, "int8": 2.0}
MAX_CER_DELTA_BY_PRECISION = {"fp32": 0.2, "fp16": 1.0, "int8": 2.0, "int4": 3.0}
MIN_SAMPLES = 500

@classmethod
Expand Down
Loading