Skip to content

Commit ae87fa0

Browse files
gramalingamCopilot
andcommitted
Fix Clef lazy QKV casting and verify persisted pipeline execution
Cast each fused attention QKV split to the target initializer dtype before the lazy streaming loader materializes it. Cover all nine float32/float16/bfloat16 source-target combinations for weights and biases through save/reload. Run multimodal pipeline parity on reloaded exported packages rather than original lazy bindings, and consistently import the public build function in Clef tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ganesan Ramalingam <grama@microsoft.com>
1 parent bc57920 commit ae87fa0

2 files changed

Lines changed: 80 additions & 10 deletions

File tree

‎src/mobius/integrations/transformers/_clef.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import onnx_ir as ir
1515
import torch
1616
from huggingface_hub import hf_hub_download
17+
from onnx_ir import tensor_adapters
1718

1819
from mobius._builder import build_from_module, resolve_dtype
1920
from mobius.integrations._weight_loading import (
@@ -69,8 +70,10 @@ def _source_name(component: str, name: str) -> tuple[str, int | None]:
6970
)
7071

7172

72-
def _split_qkv(tensor: torch.Tensor, _name: str, *, index: int) -> torch.Tensor:
73-
return tensor.chunk(3, dim=0)[index].contiguous()
73+
def _split_qkv(
74+
tensor: torch.Tensor, _name: str, *, index: int, dtype: torch.dtype
75+
) -> torch.Tensor:
76+
return tensor.chunk(3, dim=0)[index].to(dtype=dtype).contiguous()
7477

7578

7679
def _plan(
@@ -111,7 +114,11 @@ def _plan(
111114
expected_source_dtype=source_dtype,
112115
expected_target_shape=target_shape,
113116
expected_target_dtype=value.dtype,
114-
transform=functools.partial(_split_qkv, index=split),
117+
transform=functools.partial(
118+
_split_qkv,
119+
index=split,
120+
dtype=tensor_adapters.to_torch_dtype(value.dtype),
121+
),
115122
)
116123
ignored = {}
117124
for source in index.keys() - used:

‎src/mobius/models/clef_test.py‎

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
import dataclasses
7+
import functools
78
import importlib.util
89
import json
910
import pathlib
@@ -16,7 +17,7 @@
1617
import pytest
1718
import torch
1819

19-
from mobius import build_from_module
20+
from mobius import build, build_from_module
2021
from mobius._configs import VisionConfig
2122
from mobius._model_package import ModelPackage
2223
from mobius._testing import make_config
@@ -382,6 +383,71 @@ def test_streaming_plan_is_fail_closed_and_supports_tied_embeddings():
382383
_asset(str(pathlib.Path(__file__).parent), "does_not_exist.safetensors", None)
383384

384385

386+
@pytest.mark.parametrize("source_dtype", [torch.float32, torch.float16, torch.bfloat16])
387+
@pytest.mark.parametrize(
388+
"target_dtype", [ir.DataType.FLOAT, ir.DataType.FLOAT16, ir.DataType.BFLOAT16]
389+
)
390+
def test_streamed_qkv_dtype_conversion_survives_save_and_reload(
391+
tmp_path, source_dtype, target_dtype
392+
):
393+
from onnx_ir import tensor_adapters
394+
from safetensors.torch import save_file
395+
396+
from mobius.integrations._weight_loading import stream_preprocessed_safetensors_to_model
397+
from mobius.integrations.transformers._clef import _plan
398+
399+
sources = {
400+
"evidence_layers.0.attention.in_proj_weight": torch.arange(48)
401+
.reshape(12, 4)
402+
.to(source_dtype),
403+
"evidence_layers.0.attention.in_proj_bias": torch.arange(12).to(source_dtype),
404+
}
405+
checkpoint = tmp_path / "joint_head.safetensors"
406+
save_file(sources, str(checkpoint))
407+
initializers = [
408+
ir.Value(
409+
name=f"evidence_layers.0.attention.{projection}.{suffix}",
410+
shape=ir.Shape([4, 4] if suffix == "weight" else [4]),
411+
type=ir.TensorType(target_dtype),
412+
)
413+
for projection in ("q_proj", "k_proj", "v_proj")
414+
for suffix in ("weight", "bias")
415+
]
416+
graph = ir.Graph(
417+
[],
418+
initializers,
419+
nodes=[],
420+
initializers=initializers,
421+
name="qkv",
422+
opset_imports={"": 24},
423+
)
424+
model = ir.Model(graph, ir_version=11)
425+
stream_preprocessed_safetensors_to_model(
426+
model,
427+
str(tmp_path),
428+
functools.partial(
429+
_plan, component="decision_head", recognized=set(sources), tied_embeddings=False
430+
),
431+
_resolved_paths=[str(checkpoint)],
432+
)
433+
output = tmp_path / "onnx"
434+
ModelPackage({"model": model}).save(str(output), progress_bar=False)
435+
reloaded = ModelPackage.load(str(output))["model"]
436+
for index, projection in enumerate(("q_proj", "k_proj", "v_proj")):
437+
for suffix in ("weight", "bias"):
438+
value = reloaded.graph.initializers[
439+
f"evidence_layers.0.attention.{projection}.{suffix}"
440+
]
441+
assert value.dtype == target_dtype
442+
actual = value.const_value.numpy()
443+
expected = (
444+
sources[f"evidence_layers.0.attention.in_proj_{suffix}"]
445+
.chunk(3)[index]
446+
.to(tensor_adapters.to_torch_dtype(target_dtype))
447+
)
448+
np.testing.assert_array_equal(actual.astype(np.float32), expected.float().numpy())
449+
450+
385451
@pytest.mark.parametrize("option", ["text_only", "export_paged_attention", "fp8_kv_cache"])
386452
def test_autoregressive_build_options_are_rejected(option):
387453
from mobius.integrations.transformers._builder import build_transformers_model
@@ -431,9 +497,7 @@ def test_cli_export_writes_advisory_contract_for_clef(tmp_path):
431497

432498
@pytest.mark.integration
433499
def test_published_checkpoint_builds_all_decision_components():
434-
import mobius
435-
436-
package = mobius.build(CLEF_FLASH_MODEL_ID, load_weights=False, dtype="f32")
500+
package = build(CLEF_FLASH_MODEL_ID, load_weights=False, dtype="f32")
437501
assert set(package) == {"decoder", "vision_encoder", "embedding", "decision_head"}
438502
assert package.config.hidden_size == 4096
439503
assert package.config.head_width == 1024
@@ -630,8 +694,6 @@ def test_streamed_full_pipeline_matches_upstream_for_text_image_and_video(
630694
from safetensors.torch import save_file
631695
from transformers import Qwen3_5Config, Qwen3_5ForConditionalGeneration
632696

633-
import mobius
634-
635697
torch_dtype = torch.float32 if dtype == "f32" else torch.float16
636698
np_dtype = np.float32 if dtype == "f32" else np.float16
637699
tolerance = 1e-4 if dtype == "f32" else 1e-2
@@ -689,13 +751,14 @@ def test_streamed_full_pipeline_matches_upstream_for_text_image_and_video(
689751
save_file(reference.state_dict(), str(tmp_path / "model.safetensors"))
690752
save_file(head.state_dict(), str(tmp_path / "joint_head.safetensors"))
691753
(tmp_path / "joint_head_config.json").write_text(json.dumps(head_config))
692-
package = mobius.build(str(tmp_path), dtype=dtype)
754+
package = build(str(tmp_path), dtype=dtype)
693755
assert package.config.partial_rotary_factor == pytest.approx(0.5)
694756
assert package.config.rope_theta == 10000
695757
assert package.config.mrope_section == [1, 1, 0]
696758
assert package.weight_loading_report["components"]["decision_head"]["assigned_tensors"] > 0
697759
# Serialize the lazy bindings, then execute each preceding ONNX stage.
698760
package.save(str(tmp_path / "onnx"), progress_bar=False, max_workers=1)
761+
package = ModelPackage.load(str(tmp_path / "onnx"))
699762
sessions = {name: OnnxModelSession(model) for name, model in package.items()}
700763
grid = torch.tensor([[1, 4, 4]])
701764
for modalities in ((), ("image",), ("video",), ("video", "image")):

0 commit comments

Comments
 (0)