|
| 1 | +"""Model.load(zip) + translate(text): greedy KV decode with plain fallback.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +import numpy as np |
| 8 | + |
| 9 | +from interscript_ml.tokens import EOS_ID, PAD_ID, decode, encode |
| 10 | +from interscript_ml.loader import load_manifest, verify_and_read |
| 11 | + |
| 12 | + |
| 13 | +class Model: |
| 14 | + """A loaded, checksum-verified IMF v1 model. |
| 15 | +
|
| 16 | + >>> model = Model.load("khm-latn-1.0.zip") |
| 17 | + >>> model.translate("ភាសា") |
| 18 | + """ |
| 19 | + |
| 20 | + def __init__(self, zip_path: Path | str): |
| 21 | + self.zip_path = Path(zip_path) |
| 22 | + self.manifest = load_manifest(self.zip_path) |
| 23 | + import onnxruntime as ort |
| 24 | + |
| 25 | + options = ort.SessionOptions() |
| 26 | + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL |
| 27 | + graphs = verify_and_read(self.zip_path) |
| 28 | + self._encoder = ort.InferenceSession( |
| 29 | + graphs["encoder.onnx"], options, providers=_providers() |
| 30 | + ) |
| 31 | + decoder_name = ( |
| 32 | + "decoder-kv.onnx" |
| 33 | + if self.manifest.decoder == "kv" and "decoder-kv.onnx" in graphs |
| 34 | + else "decoder.onnx" |
| 35 | + ) |
| 36 | + self._kv_session = decoder_name == "decoder-kv.onnx" |
| 37 | + self._decoder = ort.InferenceSession( |
| 38 | + graphs[decoder_name], options, providers=_providers() |
| 39 | + ) |
| 40 | + self._pasts = { |
| 41 | + meta.name: _zero_past(meta) |
| 42 | + for meta in self._decoder.get_inputs() |
| 43 | + if meta.name.startswith("past_") |
| 44 | + } |
| 45 | + self._output_names = [o.name for o in self._decoder.get_outputs()] |
| 46 | + |
| 47 | + @classmethod |
| 48 | + def load(cls, path: Path | str) -> "Model": |
| 49 | + return cls(path) |
| 50 | + |
| 51 | + @property |
| 52 | + def id(self) -> str: |
| 53 | + return self.manifest.id |
| 54 | + |
| 55 | + def translate(self, text: str, max_len: int = 256) -> str: |
| 56 | + token_ids = self.generate(text, max_len=max_len) |
| 57 | + return decode(token_ids) |
| 58 | + |
| 59 | + def generate(self, text: str, max_len: int = 256) -> list[int]: |
| 60 | + ids = np.array([encode(text)], dtype=np.int64) |
| 61 | + if ids.shape[1] == 1: # only the trailing EOS: empty input |
| 62 | + return [] |
| 63 | + hidden = self._encoder.run(None, {"input_ids": ids})[0] |
| 64 | + if self._kv_session: |
| 65 | + return self._greedy_kv(hidden, max_len) |
| 66 | + return self._greedy_plain(hidden, max_len) |
| 67 | + |
| 68 | + def _greedy_kv(self, hidden, max_len: int) -> list[int]: |
| 69 | + pasts = dict(self._pasts) |
| 70 | + current = np.array([[PAD_ID]], dtype=np.int64) |
| 71 | + generated: list[int] = [] |
| 72 | + for _ in range(max_len): |
| 73 | + outputs = self._decoder.run( |
| 74 | + None, |
| 75 | + {"input_ids": current, "encoder_hidden_states": hidden, **pasts}, |
| 76 | + ) |
| 77 | + results = dict(zip(self._output_names, outputs, strict=True)) |
| 78 | + token = int(np.argmax(results["logits"][0, -1])) |
| 79 | + if token == EOS_ID: |
| 80 | + break |
| 81 | + generated.append(token) |
| 82 | + pasts = { |
| 83 | + name: results[name.replace("past_", "present_", 1)] |
| 84 | + for name in pasts |
| 85 | + } |
| 86 | + current = np.array([[token]], dtype=np.int64) |
| 87 | + return generated |
| 88 | + |
| 89 | + def _greedy_plain(self, hidden, max_len: int) -> list[int]: |
| 90 | + decoder_ids = np.array([[PAD_ID]], dtype=np.int64) |
| 91 | + generated: list[int] = [] |
| 92 | + for _ in range(max_len): |
| 93 | + logits = self._decoder.run( |
| 94 | + None, |
| 95 | + {"input_ids": decoder_ids, "encoder_hidden_states": hidden}, |
| 96 | + )[0] |
| 97 | + token = int(np.argmax(logits[0, -1])) |
| 98 | + if token == EOS_ID: |
| 99 | + break |
| 100 | + generated.append(token) |
| 101 | + decoder_ids = np.concatenate( |
| 102 | + [decoder_ids, np.array([[token]], dtype=np.int64)], axis=1 |
| 103 | + ) |
| 104 | + return generated |
| 105 | + |
| 106 | + |
| 107 | +def _providers() -> list[str]: |
| 108 | + import onnxruntime as ort |
| 109 | + |
| 110 | + available = ort.get_available_providers() |
| 111 | + preferred = [p for p in ("CPUExecutionProvider",) if p in available] |
| 112 | + return preferred or available |
| 113 | + |
| 114 | + |
| 115 | +def _zero_past(meta) -> object: |
| 116 | + shape = meta.shape # [batch, heads, past_seq, d_kv], dynamic dims are str |
| 117 | + heads = shape[1] if isinstance(shape[1], int) else 4 |
| 118 | + d_kv = shape[3] if isinstance(shape[3], int) else 8 |
| 119 | + dtype = np.float16 if meta.type == "tensor(float16)" else np.float32 |
| 120 | + return np.zeros((1, heads, 0, d_kv), dtype=dtype) |
0 commit comments