Skip to content

Commit e8a9b16

Browse files
committed
Compare the module trees, not two compilations of them
tests/vllm/_granite4_fullsize_tests.py::test_logits_match[4.0-micro] fails on the vllm20 CI leg and passes on vllm19, and has done so on unrelated PRs -- 117 (2f919be) is a docs-and-comments-only commit, which cannot move a logit. So the assertion is measuring something other than the code under test. What it measures is compilation. The test asserts atol=rtol=0 between two separately launched vLLM engines: upstream GraniteMoeHybrid and GraniteSwitch with num_adapters=0. Those are different graphs, so torch.compile and inductor are free to fuse and tile them differently, and at production dimensions that is enough to round one bf16 logit's last bit apart. vLLM logs its own per-engine kernel choice: the failing engine reported IrOpPriorityConfig(rms_norm= ['native']) where its sibling reported ['vllm_c', 'native'] -- a different RMSNorm kernel in 80 places. The attribution rests on what does pass. 4.0-1b and 4.0-350m have the same 40 dense layers and are bit-exact on both legs; the same comparison on the HF backend is bit-exact for all three; and real granite-4.0-micro weights are bit-exact through vLLM in test_skinning_equivalence.py. A weight-transfer or skinning bug would not spare all of those. So run these eager. Bit-exactness is a property of the module tree, and eager is where the module tree is what executes -- enforce_eager takes the compiler, the CUDA-graph capture and vLLM's on-disk compile cache out of the comparison and leaves the gate at atol=rtol=0 rather than loosening it. There was already precedent for the flag in _granite4_mini_tests.py's _UPSTREAM_EAGER_CONFIGS. The compiled path keeps the coverage it already had, from the two tests that are deliberately tolerant of a flipped last bit: test_skinning_equivalence.py on real weights and test_generation_equivalence.py on distributions. Two supporting changes, because the failure was also unreadable: * The bit-exact branch now calls this repo's assert_close instead of torch.testing.assert_close. A msg= string handed to the latter *replaces* its whole diagnostic, which is why four CI runs recorded "logprobs should be bit-exact" and not one number -- there was no way to tell a last-bit drift from a real regression without re-running 4 GPU-hours. assert_close reports the count, the worst diff and its magnitude. * assert_close now treats matching non-finite entries as equal. Logprob tensors are padded with -inf for positions vLLM did not return, and -inf - -inf is nan, which fails every comparison including diff <= 0.0. That trap was latent while only the tolerance branch used the helper; the bit-exact branch would have hit it as a bogus worst=nan. The margin is printed on a pass too, so a gate that has quietly started riding at one ULP is visible before it trips. Verified on CPU: 35 tests in tests/hf/test_granite4_mini.py and test_granitemoe_mini.py (the existing assert_close callers) still pass, the three TestGranite4FullSizeWeightTransfer cases pass, and the five assert_close semantics above were checked directly. The GPU legs are the real gate. Signed-off-by: itzikvaisman <yizhak.vaisman@ibm.com>
1 parent 24aa420 commit e8a9b16

2 files changed

Lines changed: 64 additions & 17 deletions

File tree

‎tests/shared/granite4_equivalence.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,22 @@ def assert_close(actual, expected, *, atol, rtol, msg=""):
3131
3232
This avoids the joint formula (atol + rtol * |b|) where the two
3333
tolerances inflate each other's budget.
34+
35+
``atol=rtol=0.0`` is a valid bit-exact gate. Passing it here rather than
36+
to ``torch.testing.assert_close`` is deliberate: that function's ``msg=``
37+
string *replaces* its whole diagnostic, so a failure reports only the
38+
message and none of the numbers needed to tell a last-bit drift from a
39+
real regression.
40+
41+
Matching non-finite entries count as equal. Logprob tensors are padded
42+
with ``-inf`` for positions vLLM did not return, and ``-inf - -inf`` is
43+
``nan``, which fails every comparison — including ``diff <= 0.0`` — and
44+
would surface as a bogus ``worst=nan``.
3445
"""
3546
diff = (actual - expected).abs()
3647
abs_ok = diff <= atol
3748
rel_ok = diff <= rtol * expected.abs()
38-
ok = abs_ok | rel_ok
49+
ok = abs_ok | rel_ok | (actual == expected)
3950

4051
if not ok.all():
4152
num_bad = (~ok).sum().item()
@@ -256,6 +267,21 @@ def get_tolerances(layer_types, long_sequence=False, has_kv_hidden=False):
256267
position while upstream embeds the original control id. Visible
257268
positions attending to the control position pick up that delta.
258269
270+
Case 1's bit-exactness holds for the *eager* graph only. Under
271+
torch.compile the switch graph and the upstream graph are different
272+
graphs, so inductor is free to fuse and tile them differently; at
273+
production dimensions that is enough to round one bf16 logit's last bit
274+
apart. It surfaced as ``test_logits_match[4.0-micro]`` failing on vLLM
275+
0.20 while passing on 0.19 — with 4.0-1b and 4.0-350m (same 40 layers,
276+
same dense stack) passing on both, and the same test bit-exact on the HF
277+
backend, which is why the divergence is attributed to compilation rather
278+
than to the weight transfer. vLLM's per-engine kernel choice is visible
279+
in its own logs: the failing engine reported
280+
``IrOpPriorityConfig(rms_norm=['native'])`` where its sibling reported
281+
``['vllm_c', 'native']``. The vLLM callers that want a bit-exact gate
282+
therefore pass ``enforce_eager=True``; see
283+
``tests/vllm/_granite4_fullsize_tests.py``.
284+
259285
Args:
260286
layer_types: list of "attention" strings
261287
long_sequence: unused (kept for API compatibility)

‎tests/vllm/_granite4_fullsize_tests.py‎

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
44
Full-size Granite 4 equivalence tests via vllm.LLM.
55
Requires CUDA GPU and vLLM installed.
6+
7+
Runs eager. These are the bit-exact tests, and bit-exactness is a property of
8+
the module tree, not of whatever inductor decided to fuse in each of the two
9+
graphs — see the comment on ``enforce_eager`` below.
610
"""
711

812
import pytest
@@ -53,22 +57,39 @@ def test_logits_match(self, model_name, tmp_path):
5357
tmpdir=tmp_path,
5458
max_model_len=64,
5559
gpu_memory_utilization=0.4,
60+
# Compare the two module trees, not two compilations of them.
61+
# torch.compile/inductor pick fusions and kernels per graph, and
62+
# the switch graph is not the upstream graph, so at production
63+
# dimensions the compiled paths can land a bf16 last bit
64+
# differently while the eager math is identical. That is what
65+
# broke 4.0-micro under vLLM 0.20 and not 0.19 (see the note in
66+
# get_tolerances). enforce_eager removes the compiler, the
67+
# CUDA-graph capture and vLLM's on-disk compile cache from the
68+
# comparison, which restores a meaningful bit-exact gate.
69+
# The compiled path is still covered, by the tests that are
70+
# tolerant of a flipped last bit:
71+
# tests/composer/test_skinning_equivalence.py (real weights) and
72+
# tests/vllm/test_generation_equivalence.py (distribution).
73+
enforce_eager=True,
5674
)
5775

5876
tol = get_tolerances(layer_types)
59-
if tol is None:
60-
torch.testing.assert_close(
61-
switch,
62-
upstream,
63-
atol=0.0,
64-
rtol=0.0,
65-
msg=f"{model_name}: logprobs should be bit-exact",
66-
)
67-
else:
68-
assert_close(
69-
switch,
70-
upstream,
71-
atol=tol[0],
72-
rtol=tol[1],
73-
msg=f"{model_name}: full-size logprobs diverge",
74-
)
77+
atol, rtol = (0.0, 0.0) if tol is None else tol
78+
assert_close(
79+
switch,
80+
upstream,
81+
atol=atol,
82+
rtol=rtol,
83+
msg=f"{model_name}: full-size logprobs diverge",
84+
)
85+
86+
# Print the margin on a pass, not only into the failure message. A
87+
# bit-exact gate that has quietly started riding at one ULP is a
88+
# regression in progress, and this is the only place it shows.
89+
delta = (switch - upstream).abs()
90+
finite = delta[delta.isfinite()]
91+
print(
92+
f"{model_name}: {finite.numel()} finite logprob entries, "
93+
f"max |delta| = {finite.max().item():.3e} "
94+
f"(atol={atol:.1e} rtol={rtol:.1e})"
95+
)

0 commit comments

Comments
 (0)