Skip to content

Commit c830b97

Browse files
titaiwangmsCopilot
andcommitted
Validate QMoE scales and defer FC1 interleaving
Reject invalid legacy uniform INT4 scales and serialize FC1 transforms without caching duplicate checkpoint payloads. Preserve component-local transform routing and cover asymmetric zero points and distinct multilayer payloads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fba8a408-cd63-49f0-b8b4-5daa19ed42d4 Signed-off-by: titaiwang <titaiwang@microsoft.com>
1 parent 0444c12 commit c830b97

13 files changed

Lines changed: 471 additions & 87 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,20 @@ FC scale dtypes must match the activations. Every projection's scales must be
7878
finite and nonnegative. When the adapter supplies the activation dtype,
7979
scales must remain finite after conversion. Non-INT3 layouts retain the
8080
standard weight loader's dtype conversion.
81+
The legacy uniform INT4 path receives the same numeric scale checks.
8182
INT2, INT4, and INT8 projections retain their existing zero-point
8283
semantics; asymmetric zero points must be packed `uint8` sidecars.
8384
Dense INT3 `MatMulNBits`, embeddings, other source layouts, and schema-unapproved
8485
tails are not enabled. Keep non-expert projections at supported precisions.
8586

87+
`build()` defers INT3-layout FC1 interleaving to initializer serialization,
88+
using uncached lazy tensors instead of retaining a second complete FC1 payload.
89+
The original checkpoint is still loaded eagerly; this bounds additional
90+
interleaving storage, not total checkpoint memory. Direct `preprocess_weights()`
91+
calls retain eager behavior unless an output `weight_transforms` mapping is
92+
provided. Pass that mapping to `ModelPackage.apply_weights()` with the returned
93+
weights to preserve the deferred transformations.
94+
8695
**Export support is not a runtime qualification matrix.** INT3 QMoE nodes
8796
carry a draft-format/runtime-warning marker; they do not invent an
8897
approved schema version. Execution of every pair depends on the target ORT

‎src/mobius/_model_package.py‎

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
from mobius._export_report import ComponentExportReport
4545
from mobius._optimizations import fold_initializers_after_weights
46+
from mobius._weight_utils import WeightTransform
4647
from mobius.adapters import (
4748
AdapterArtifact,
4849
AdapterServiceOptions,
@@ -1415,6 +1416,7 @@ def apply_weights(
14151416
prefix_map: dict[str, str] | None = None,
14161417
*,
14171418
fold_constants: bool = True,
1419+
weight_transforms: dict[str, WeightTransform] | None = None,
14181420
) -> None:
14191421
"""Apply weights from a state dict across component models.
14201422
@@ -1432,48 +1434,63 @@ def apply_weights(
14321434
Weights whose name starts with a prefix are applied to the
14331435
named component (with the prefix stripped). Unmatched weights
14341436
are applied to all components.
1437+
weight_transforms: Shape-preserving transforms keyed by names in
1438+
``state_dict`` (the preprocessed names, before prefix routing),
1439+
evaluated without caching during serialization.
14351440
"""
14361441
applied: set[str] = set()
14371442

14381443
if len(self.data) == 1:
14391444
# Single component — apply all weights directly
14401445
model = next(iter(self.data.values()))
1441-
applied = _apply_weights_to_model(model, state_dict)
1446+
applied = _apply_weights_to_model(model, state_dict, weight_transforms)
14421447
elif prefix_map is None:
14431448
# No routing info — try every weight against every model
14441449
for model in self.data.values():
1445-
applied |= _apply_weights_to_model(model, state_dict)
1450+
applied |= _apply_weights_to_model(model, state_dict, weight_transforms)
14461451
else:
14471452
# Route by prefix
14481453
routed: dict[str, dict[str, torch.Tensor]] = {name: {} for name in self.data}
1454+
routed_transforms: dict[str, dict[str, WeightTransform]] = {
1455+
name: {} for name in self.data
1456+
}
14491457
unmatched: dict[str, torch.Tensor] = {}
1450-
# Track original HF names for weights that get stripped
1451-
stripped_to_original: dict[str, str] = {}
1458+
stripped_to_original: dict[tuple[str, str], str] = {}
14521459

14531460
for weight_name, tensor in state_dict.items():
14541461
matched = False
14551462
for prefix, component in prefix_map.items():
14561463
if weight_name.startswith(prefix):
14571464
stripped = weight_name[len(prefix) :].lstrip(".")
14581465
routed[component][stripped] = tensor
1459-
stripped_to_original[stripped] = weight_name
1466+
if weight_transforms is not None and weight_name in weight_transforms:
1467+
routed_transforms[component][stripped] = weight_transforms[
1468+
weight_name
1469+
]
1470+
stripped_to_original[(component, stripped)] = weight_name
14601471
matched = True
14611472
break
14621473
if not matched:
14631474
unmatched[weight_name] = tensor
14641475

14651476
for component_name, component_weights in routed.items():
14661477
applied_stripped = _apply_weights_to_model(
1467-
self.data[component_name], component_weights
1478+
self.data[component_name],
1479+
component_weights,
1480+
routed_transforms[component_name],
14681481
)
14691482
for s in applied_stripped:
1470-
applied.add(stripped_to_original.get(s, s))
1483+
applied.add(stripped_to_original.get((component_name, s), s))
14711484

14721485
# Try unmatched weights against all models
14731486
if unmatched:
14741487
for model in self.data.values():
1475-
applied |= _apply_weights_to_model(model, unmatched)
1488+
applied |= _apply_weights_to_model(model, unmatched, weight_transforms)
14761489

1490+
if weight_transforms is not None:
1491+
unused = set(weight_transforms) - applied
1492+
if unused:
1493+
raise ValueError(f"Weight transforms were not bound: {sorted(unused)!r}.")
14771494
_log_weight_mapping(state_dict, applied)
14781495

14791496
if not fold_constants:
@@ -1629,7 +1646,11 @@ def _log_weight_mapping(
16291646
)
16301647

16311648

1632-
def _apply_weights_to_model(model: ir.Model, state_dict: dict[str, torch.Tensor]) -> set[str]:
1649+
def _apply_weights_to_model(
1650+
model: ir.Model,
1651+
state_dict: dict[str, torch.Tensor],
1652+
weight_transforms: dict[str, WeightTransform] | None = None,
1653+
) -> set[str]:
16331654
"""Apply weights to a single model (internal helper).
16341655
16351656
Uses :func:`_assign_weight` from ``_weight_loading`` for shape
@@ -1643,6 +1664,11 @@ def _apply_weights_to_model(model: ir.Model, state_dict: dict[str, torch.Tensor]
16431664
if name not in model.graph.initializers:
16441665
continue
16451666

1646-
_assign_weight(model.graph.initializers[name], tensor, name)
1667+
_assign_weight(
1668+
model.graph.initializers[name],
1669+
tensor,
1670+
name,
1671+
transform=weight_transforms.get(name) if weight_transforms is not None else None,
1672+
)
16471673
applied.add(name)
16481674
return applied

‎src/mobius/_model_package_test.py‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,107 @@ def test_save_preserves_public_policy_symbols(self, tmp_path):
14401440

14411441

14421442
class TestModelPackageApplyWeights:
1443+
@pytest.mark.parametrize("second_has_initializer", [False, True])
1444+
def test_weight_transform_accounting_separates_component_local_names(
1445+
self, second_has_initializer
1446+
):
1447+
values = {}
1448+
models = {}
1449+
for component in ("text", "vision"):
1450+
graph = ir.Graph([], [], nodes=[], name=component)
1451+
if component == "text" or second_has_initializer:
1452+
value = ir.Value(
1453+
name="weight",
1454+
type=ir.TensorType(ir.DataType.FLOAT),
1455+
shape=ir.Shape([2, 3]),
1456+
const_value=ir.tensor(torch.zeros(2, 3)),
1457+
)
1458+
graph.register_initializer(value)
1459+
values[component] = value
1460+
models[component] = ir.Model(graph, ir_version=10)
1461+
package = ModelPackage(models)
1462+
weights = {"text.weight": torch.zeros(2, 3), "vision.weight": torch.ones(2, 3)}
1463+
transforms = {
1464+
"text.weight": lambda tensor: tensor + 2,
1465+
"vision.weight": lambda tensor: tensor + 3,
1466+
}
1467+
if not second_has_initializer:
1468+
with pytest.raises(ValueError, match=r"not bound.*vision\.weight"):
1469+
package.apply_weights(
1470+
weights,
1471+
prefix_map={"text.": "text", "vision.": "vision"},
1472+
weight_transforms={"vision.weight": transforms["vision.weight"]},
1473+
fold_constants=False,
1474+
)
1475+
else:
1476+
package.apply_weights(
1477+
weights,
1478+
prefix_map={"text.": "text", "vision.": "vision"},
1479+
weight_transforms=transforms,
1480+
fold_constants=False,
1481+
)
1482+
for component in ("text", "vision"):
1483+
actual = torch.from_numpy(values[component].const_value.numpy())
1484+
expected = transforms[component + ".weight"](weights[component + ".weight"])
1485+
assert torch.equal(actual, expected)
1486+
1487+
@pytest.mark.parametrize("routed", [False, True])
1488+
def test_weight_transforms_are_lazy_and_follow_prefix_routing(self, routed):
1489+
value = ir.Value(
1490+
name="weight",
1491+
type=ir.TensorType(ir.DataType.FLOAT),
1492+
shape=ir.Shape([2, 3]),
1493+
const_value=ir.tensor(torch.zeros(2, 3)),
1494+
)
1495+
graph = ir.Graph([], [], nodes=[], name="text")
1496+
graph.register_initializer(value)
1497+
package = ModelPackage(
1498+
{"text": ir.Model(graph, ir_version=10), "other": _make_simple_model()}
1499+
)
1500+
calls = []
1501+
source = torch.arange(6).reshape(2, 3).float()
1502+
1503+
def transform(tensor):
1504+
calls.append(tensor.data_ptr())
1505+
return tensor + 1
1506+
1507+
name = "text.weight" if routed else "weight"
1508+
package.apply_weights(
1509+
{name: source},
1510+
prefix_map={"text.": "text"} if routed else None,
1511+
weight_transforms={name: transform},
1512+
fold_constants=False,
1513+
)
1514+
assert calls == []
1515+
assert isinstance(value.const_value, ir.LazyTensor)
1516+
assert not value.const_value.cache
1517+
for count in (1, 2):
1518+
assert torch.equal(torch.from_numpy(value.const_value.numpy()), source + 1)
1519+
assert len(calls) == count
1520+
1521+
def test_unbound_weight_transform_raises(self):
1522+
package = ModelPackage({"model": _make_simple_model()})
1523+
with pytest.raises(ValueError, match="Weight transforms were not bound"):
1524+
package.apply_weights({}, weight_transforms={"missing": lambda tensor: tensor})
1525+
1526+
def test_weight_transform_changed_shape_raises(self):
1527+
value = ir.Value(
1528+
name="weight",
1529+
type=ir.TensorType(ir.DataType.FLOAT),
1530+
shape=ir.Shape([2, 3]),
1531+
const_value=ir.tensor(torch.zeros(2, 3)),
1532+
)
1533+
graph = ir.Graph([], [], nodes=[], name="model")
1534+
graph.register_initializer(value)
1535+
package = ModelPackage({"model": ir.Model(graph, ir_version=10)})
1536+
package.apply_weights(
1537+
{"weight": torch.zeros(2, 3)},
1538+
weight_transforms={"weight": lambda tensor: tensor.reshape(3, 2)},
1539+
fold_constants=False,
1540+
)
1541+
with pytest.raises(ValueError, match="changed shape"):
1542+
value.const_value.numpy()
1543+
14431544
def test_single_component(self):
14441545
config = make_config()
14451546
module = CausalLMModel(config)

0 commit comments

Comments
 (0)