Skip to content

Commit 1cf2447

Browse files
rui-renCopilot
andcommitted
Preserve native Bolt routing normalization parity
Select denominator routes with repeated ArgMax and lower-index tie breaking instead of CUDA TopK. Accumulate unbiased mixing weights in selected-rank FP32 order instead of a tree ReduceSum, preserving the validated V3 native routing arithmetic before scaling and the BF16 output store. Add exact CPU/CUDA coefficient regressions for cutoff ties, non-associative sums, zero weights, top-k boundaries, and unnormalized scaling. The new sum-order cases fail against the previous emitter. Weights, recurrence, cache interfaces, and the ORT runtime are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: rui-ren <ruiren1225@gmail.com>
1 parent d3fe95f commit 1cf2447

2 files changed

Lines changed: 52 additions & 17 deletions

File tree

‎src/mobius/models/_nemotron_h_omni_packed.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,22 @@ def emit(op):
334334
# routing, prepare a row denominator from the same full selection
335335
# scores; MoEPacked still selects and gathers its own expert routes.
336336
if normalize:
337-
_, chosen = op.TopK(scores, [topk], axis=-1, largest=1, sorted=1, _outputs=2)
338-
chosen_weights = op.GatherElements(mixing, chosen, axis=-1)
339-
total = op.ReduceSum(chosen_weights, [-1], keepdims=1)
337+
# Match native lower-index tie breaking and rank-ordered FP32
338+
# addition; CUDA TopK and tree reductions can change the denominator.
339+
remaining = scores
340+
total = None
341+
mask = None
342+
for rank in range(topk):
343+
chosen = op.ArgMax(remaining, axis=-1, keepdims=1, select_last_index=0)
344+
chosen_weight = op.GatherElements(mixing, chosen, axis=-1)
345+
total = chosen_weight if total is None else op.Add(total, chosen_weight)
346+
if rank + 1 < topk:
347+
if mask is None:
348+
mask = op.ConstantOfShape(
349+
op.Shape(chosen),
350+
value=ir.tensor([-float("inf")], dtype=ir.DataType.FLOAT),
351+
)
352+
remaining = op.ScatterElements(remaining, chosen, mask, axis=-1)
340353
mixing = op.Div(mixing, op.Add(total, 1e-20))
341354
mixing = op.Mul(mixing, layer.routed_scaling_factor)
342355
normalize = 0

‎src/mobius/models/_nemotron_h_omni_packed_test.py‎

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,7 @@ def test_compact_decoder_wires_full_fp32_routing_without_compact_ids(normalize,
207207
assert scaled.op_type == "Mul"
208208
assert scaled.inputs[1].const_value.numpy().item() == np.float32(scale)
209209
assert next(iter(consumer.outputs[0].uses())).node.op_type != "Cast"
210-
assert sum(node.op_type == "TopK" for node in graph) == (
211-
len(consumers) if normalize and scale != 1.0 else 0 # noqa: RUF069
212-
)
210+
assert not any(node.op_type == "TopK" for node in graph)
213211
assert not any(
214212
node.op_type in {"MoEPackedIQ2XXS", "MatMulPackedIQ2XXS", "MatMulPackedQ8"}
215213
for node in graph.all_nodes()
@@ -225,31 +223,55 @@ def test_compact_decoder_wires_full_fp32_routing_without_compact_ids(normalize,
225223
)
226224

227225

228-
@pytest.mark.parametrize("topk", [1, 2])
226+
@pytest.mark.parametrize("provider", ["CPUExecutionProvider", "CUDAExecutionProvider"])
227+
@pytest.mark.parametrize("topk", [1, 2, 8])
229228
@pytest.mark.parametrize("normalize", [False, True])
230-
def test_compact_decoder_prescales_selected_mixing_in_fp32(tmp_path, topk, normalize):
231-
graph = optimize_nemotron_h_omni_cuda(_fixture(topk=topk, normalize=normalize, scale=5.0))[
232-
"decoder"
233-
].graph
229+
def test_compact_decoder_prescales_selected_mixing_in_fp32(
230+
tmp_path, topk, normalize, provider
231+
):
232+
if provider not in ort.get_available_providers():
233+
pytest.skip(f"{provider} is unavailable")
234+
graph = optimize_nemotron_h_omni_cuda(
235+
_fixture(topk=topk, experts=8, normalize=normalize, scale=5.0)
236+
)["decoder"].graph
234237
router = next(node for node in graph if node.op_type == "MoEGroupRouter")
235238
consumer = next(node for node in graph if node.op_type == "MoEPacked")
236239
preparation = ir.convenience.extract(
237240
graph, inputs=list(router.outputs[1:]), outputs=[consumer.inputs[5]]
238241
)
239242
path = tmp_path / "mixing.onnx"
240243
ir.save(ir.Model(preparation, ir_version=10), path)
241-
session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
242-
scores = np.array([[0.7, 0.9, 0.9, 0.6], [0.2, 0.5, 0.1, 0.4]], np.float32)
243-
mixing = np.array([[0.8, 0.2, 0.6, 0.9], [0, 0, 0, 0]], np.float32)
244+
session = ort.InferenceSession(str(path), providers=[provider])
245+
assert session.get_providers()[0] == provider
246+
scores = np.array(
247+
[
248+
[30, 29.577760696411133, 29.577760696411133, 4, 3, 2, 1, 0],
249+
[0, 1, 2, 3, 4, 5, 6, 7],
250+
[7, 6, 5, 4, 3, 2, 1, 0],
251+
],
252+
np.float32,
253+
)
254+
mixing = np.array(
255+
[
256+
[0.8, 0.2, 0.6, 0.9, 0.3, 0.4, 0.5, 0.1],
257+
[0, 0, 0, 0, 0, 0, 0, 0],
258+
[1, 2**-24, 2**-24, 2**-24, 2**-24, 2**-24, 2**-24, 2**-24],
259+
],
260+
np.float32,
261+
)
244262
actual = session.run(
245263
None, dict(zip([value.name for value in preparation.inputs], [scores, mixing]))
246264
)[0]
247265
chosen = np.argsort(-scores, axis=-1, kind="stable")[:, :topk]
248-
expected = np.take_along_axis(mixing, chosen, axis=-1)
266+
expected = mixing
249267
if normalize:
250-
expected = expected / (expected.sum(axis=-1, keepdims=True) + np.float32(1e-20))
268+
selected = np.take_along_axis(mixing, chosen, axis=-1)
269+
total = selected[:, :1].copy()
270+
for rank in range(1, topk):
271+
total = total + selected[:, rank : rank + 1]
272+
expected = expected / (total + np.float32(1e-20))
251273
expected = expected * np.float32(5.0)
252-
np.testing.assert_array_equal(np.take_along_axis(actual, chosen, axis=-1), expected)
274+
np.testing.assert_array_equal(actual, expected)
253275
assert actual.dtype == np.float32
254276
assert np.isfinite(actual).all()
255277

0 commit comments

Comments
 (0)