|
4 | 4 | from __future__ import annotations |
5 | 5 |
|
6 | 6 | import dataclasses |
| 7 | +import functools |
7 | 8 | import importlib.util |
8 | 9 | import json |
9 | 10 | import pathlib |
|
16 | 17 | import pytest |
17 | 18 | import torch |
18 | 19 |
|
19 | | -from mobius import build_from_module |
| 20 | +from mobius import build, build_from_module |
20 | 21 | from mobius._configs import VisionConfig |
21 | 22 | from mobius._model_package import ModelPackage |
22 | 23 | from mobius._testing import make_config |
@@ -382,6 +383,71 @@ def test_streaming_plan_is_fail_closed_and_supports_tied_embeddings(): |
382 | 383 | _asset(str(pathlib.Path(__file__).parent), "does_not_exist.safetensors", None) |
383 | 384 |
|
384 | 385 |
|
| 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 | + |
385 | 451 | @pytest.mark.parametrize("option", ["text_only", "export_paged_attention", "fp8_kv_cache"]) |
386 | 452 | def test_autoregressive_build_options_are_rejected(option): |
387 | 453 | from mobius.integrations.transformers._builder import build_transformers_model |
@@ -431,9 +497,7 @@ def test_cli_export_writes_advisory_contract_for_clef(tmp_path): |
431 | 497 |
|
432 | 498 | @pytest.mark.integration |
433 | 499 | 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") |
437 | 501 | assert set(package) == {"decoder", "vision_encoder", "embedding", "decision_head"} |
438 | 502 | assert package.config.hidden_size == 4096 |
439 | 503 | assert package.config.head_width == 1024 |
@@ -630,8 +694,6 @@ def test_streamed_full_pipeline_matches_upstream_for_text_image_and_video( |
630 | 694 | from safetensors.torch import save_file |
631 | 695 | from transformers import Qwen3_5Config, Qwen3_5ForConditionalGeneration |
632 | 696 |
|
633 | | - import mobius |
634 | | - |
635 | 697 | torch_dtype = torch.float32 if dtype == "f32" else torch.float16 |
636 | 698 | np_dtype = np.float32 if dtype == "f32" else np.float16 |
637 | 699 | 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( |
689 | 751 | save_file(reference.state_dict(), str(tmp_path / "model.safetensors")) |
690 | 752 | save_file(head.state_dict(), str(tmp_path / "joint_head.safetensors")) |
691 | 753 | (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) |
693 | 755 | assert package.config.partial_rotary_factor == pytest.approx(0.5) |
694 | 756 | assert package.config.rope_theta == 10000 |
695 | 757 | assert package.config.mrope_section == [1, 1, 0] |
696 | 758 | assert package.weight_loading_report["components"]["decision_head"]["assigned_tensors"] > 0 |
697 | 759 | # Serialize the lazy bindings, then execute each preceding ONNX stage. |
698 | 760 | package.save(str(tmp_path / "onnx"), progress_bar=False, max_workers=1) |
| 761 | + package = ModelPackage.load(str(tmp_path / "onnx")) |
699 | 762 | sessions = {name: OnnxModelSession(model) for name, model in package.items()} |
700 | 763 | grid = torch.tensor([[1, 4, 4]]) |
701 | 764 | for modalities in ((), ("image",), ("video",), ("video", "image")): |
|
0 commit comments