| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
|
|
| import numpy as np |
| import onnxruntime as ort |
| import torch |
| import torch.nn.functional as functional |
|
|
| from export_decoder_fp32 import ( |
| HEAD_DIM, |
| HIDDEN_SIZE, |
| NUM_KV_HEADS, |
| NUM_LAYERS, |
| OPSET, |
| OUTPUT_DIR, |
| REPORT_DIR, |
| VISION_TOKENS, |
| VOCAB_SIZE, |
| DecoderBase, |
| compare_outputs, |
| flatten_cache, |
| inspect_graph, |
| load_model, |
| make_past, |
| output_names, |
| past_names, |
| ) |
| from export_decoder_qdq_int8 import rewrite_graph |
| from export_decoder_unified import empty_past, empty_vision |
|
|
|
|
| class DecoderUnifiedGather(DecoderBase): |
| def forward( |
| self, |
| vision_embeds: torch.Tensor, |
| token_ids: torch.Tensor, |
| position_ids: torch.Tensor, |
| past_k0: torch.Tensor, |
| past_k1: torch.Tensor, |
| past_k2: torch.Tensor, |
| past_k3: torch.Tensor, |
| past_k4: torch.Tensor, |
| past_k5: torch.Tensor, |
| past_v0: torch.Tensor, |
| past_v1: torch.Tensor, |
| past_v2: torch.Tensor, |
| past_v3: torch.Tensor, |
| past_v4: torch.Tensor, |
| past_v5: torch.Tensor, |
| ): |
| keys = (past_k0, past_k1, past_k2, past_k3, past_k4, past_k5) |
| values = (past_v0, past_v1, past_v2, past_v3, past_v4, past_v5) |
| token_embed = functional.embedding(token_ids, self.lm_head.weight) |
| inputs_embeds = torch.cat((vision_embeds, token_embed), dim=1) |
| outputs = self.decoder( |
| inputs_embeds=inputs_embeds, |
| position_ids=position_ids, |
| past_key_values=tuple(zip(keys, values)), |
| use_cache=True, |
| return_dict=True, |
| ) |
| final_hidden_state = outputs.last_hidden_state[:, -1:, :] |
| return (self.project_logits(final_hidden_state),) + flatten_cache( |
| outputs.past_key_values |
| ) |
|
|
|
|
| def token(token_id: int) -> torch.Tensor: |
| return torch.tensor([[token_id]], dtype=torch.int32) |
|
|
|
|
| def export_graph( |
| wrapper: DecoderUnifiedGather, |
| destination: Path, |
| *, |
| custom_opsets: dict[str, int] | None = None, |
| ) -> None: |
| torch.manual_seed(41) |
| vision = torch.randn(1, VISION_TOKENS, HIDDEN_SIZE, dtype=torch.float32) |
| past = make_past(1) |
| positions = torch.arange(1, VISION_TOKENS + 2, dtype=torch.int32).unsqueeze(0) |
| dynamic_axes = { |
| "vision_embeds": {1: "vision_len"}, |
| "position_ids": {1: "input_len"}, |
| **{name: {2: "past_len"} for name in past_names()}, |
| **{name: {2: "total_len"} for name in output_names()[1:]}, |
| } |
| torch.onnx.export( |
| wrapper, |
| (vision, token(1), positions, *past), |
| destination, |
| input_names=["vision_embeds", "token_ids", "position_ids", *past_names()], |
| output_names=output_names(), |
| dynamic_axes=dynamic_axes, |
| opset_version=OPSET, |
| do_constant_folding=True, |
| dynamo=False, |
| custom_opsets=custom_opsets, |
| ) |
|
|
|
|
| def feeds(values: tuple[torch.Tensor, ...]) -> dict[str, np.ndarray]: |
| names = ["vision_embeds", "token_ids", "position_ids", *past_names()] |
| return {name: value.numpy() for name, value in zip(names, values)} |
|
|
|
|
| def validate_mode( |
| wrapper: DecoderUnifiedGather, |
| session: ort.InferenceSession, |
| label: str, |
| values: tuple[torch.Tensor, ...], |
| ) -> dict: |
| with torch.inference_mode(): |
| expected = wrapper(*values) |
| actual = session.run(None, feeds(values)) |
| return compare_outputs(label, expected, actual) |
|
|
|
|
| def validate_graph(wrapper: DecoderUnifiedGather, path: Path) -> dict: |
| session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"]) |
| torch.manual_seed(43) |
| prefill = ( |
| torch.randn(1, VISION_TOKENS, HIDDEN_SIZE, dtype=torch.float32), |
| token(1), |
| torch.arange(VISION_TOKENS + 1, dtype=torch.int32).unsqueeze(0), |
| *empty_past(), |
| ) |
| step = ( |
| empty_vision(), |
| token(4), |
| torch.tensor([[VISION_TOKENS + 1]], dtype=torch.int32), |
| *make_past(VISION_TOKENS + 1), |
| ) |
| return { |
| "prefill_zero_past": validate_mode(wrapper, session, "gather-prefill", prefill), |
| "step_empty_vision": validate_mode(wrapper, session, "gather-step", step), |
| } |
|
|
|
|
| def main() -> None: |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
| REPORT_DIR.mkdir(parents=True, exist_ok=True) |
| torch.manual_seed(0) |
| torch.set_grad_enabled(False) |
| wrapper = DecoderUnifiedGather(load_model()).eval() |
| fp32_path = OUTPUT_DIR / "decoder_unified_gather_fp32.onnx" |
| qdq_path = OUTPUT_DIR / "decoder_unified_gather_qdq_int8.onnx" |
| print(f"Exporting {fp32_path}", flush=True) |
| export_graph(wrapper, fp32_path) |
| fp32_parity = validate_graph(wrapper, fp32_path) |
| print(f"Quantizing {qdq_path}", flush=True) |
| qdq_metadata = rewrite_graph( |
| fp32_path, |
| qdq_path, |
| quantize_gather_shapes=frozenset({(VOCAB_SIZE, HIDDEN_SIZE)}), |
| ) |
| qdq_parity = validate_graph(wrapper, qdq_path) |
| if qdq_metadata["quantized_gathers"] != 1: |
| raise RuntimeError( |
| f"Expected one quantized token embedding Gather, got {qdq_metadata['quantized_gathers']}" |
| ) |
| report = { |
| "description": ( |
| "Complete six-layer Baberu decoder using an INT32 token ID and a quantized " |
| "embedding Gather instead of a full-vocabulary one-hot MatMul" |
| ), |
| "fp32": {"graph": inspect_graph(fp32_path), "parity": fp32_parity}, |
| "qdq_int8": { |
| "graph": inspect_graph(qdq_path), |
| "quantization": qdq_metadata, |
| "parity": qdq_parity, |
| }, |
| } |
| report_path = REPORT_DIR / "decoder-gather-report.json" |
| report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") |
| print(json.dumps(report, indent=2)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|