Skip to content

Commit 5c0cb53

Browse files
xinyuangui2claude
andcommitted
[megatron][vlm] GLM-5.3-Flash VLM test: replace packed-vs-alone with a fixed-shape leak check
Packed-vs-alone measures this slice's MoE sensitivity to microbatch shape (~0.13 mean |dlogprob|, reproduced on the text-only GPTModel path with length-matched rows), not cross-sample leakage. The new test keeps the shape fixed and swaps only the neighbour's image pixels (4xH100: 0.005-0.013 vs 0.47 on the swapped image itself). HF bar 0.1 -> 0.15 for the same reason (packed 0.10, alone 0.08 from HF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrJwn14CQRnnMYja3jPNxb Signed-off-by: xgui <xgui@anyscale.com>
1 parent d0aecbd commit 5c0cb53

1 file changed

Lines changed: 44 additions & 22 deletions

File tree

‎tests/backends/skyrl_train/gpu/gpu_ci/megatron/test_megatron_glm5_vlm.py‎

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
SLICE = "eatang/GLM-5.3-Flash-4layer"
3232
FULL = "zai-org/GLM-5.3-Flash"
3333
TP, EP, NUM_GPUS = 2, 4, 4
34-
MICRO_BATCH = 4
3534

3635
# (prompt, image size or None, answer). Mixed sizes so the packed stream holds images of
3736
# different token counts; one text-only row so an image-free sample sits in a packed microbatch.
@@ -82,12 +81,12 @@ def _config(model_path: str, language_model_only: bool = False) -> SkyRLTrainCon
8281
return cfg
8382

8483

85-
def _build_rows(model_path: str):
84+
def _build_rows(model_path: str, prompts=PROMPTS, seed: int = 0):
8685
"""Per row: (input_ids, answer_len, pixel_values | None, image_grid_thw | None)."""
8786
processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
88-
gen = torch.Generator().manual_seed(0)
87+
gen = torch.Generator().manual_seed(seed)
8988
rows = []
90-
for prompt, size, answer in PROMPTS:
89+
for prompt, size, answer in prompts:
9190
content = [{"type": "text", "text": prompt}]
9291
images = None
9392
if size is not None:
@@ -148,7 +147,7 @@ def _batch(processor, rows, keep=None) -> TrainingInputBatch:
148147
return data
149148

150149

151-
def _megatron_logprobs(model_path, batch, micro_batch=MICRO_BATCH, language_model_only=False) -> torch.Tensor:
150+
def _megatron_logprobs(model_path, batch, micro_batch=4, language_model_only=False) -> torch.Tensor:
152151
"""[B, response_length] logprobs from the policy forward (old/ref-logprob path)."""
153152
cfg = _config(model_path, language_model_only=language_model_only)
154153
cfg.trainer.micro_forward_batch_size_per_gpu = micro_batch
@@ -194,30 +193,53 @@ def test_glm5_vlm_forward_matches_hf(glm5_vl_slice):
194193
diff = (megatron - hf).abs()[scored]
195194
print(f"\n[glm5 vlm vs hf] mean={diff.mean().item():.4f} max={diff.max().item():.4f}")
196195
# The truncated slice has a spread next-token distribution: bf16 noise alone is ~0.05 mean
197-
# |dlogprob| (see the glm-5.3-flash rows in test_megatron_models.py). Wrong image placement
198-
# or a dropped vision weight is far above that.
199-
assert diff.mean().item() < 0.1
196+
# |dlogprob| (see the glm-5.3-flash rows in test_megatron_models.py), and its MoE routing is
197+
# sensitive to microbatch shape (see test_glm5_vlm_no_cross_sample_leak): packed microbatches
198+
# land at ~0.10 from HF, each sample alone at ~0.08, on the text-only path too. Wrong image
199+
# placement or a dropped vision weight moves answers by ~0.5 (the image-swap control).
200+
assert diff.mean().item() < 0.15
201+
202+
203+
# A text row and an image row with the same token count; the image is drawn with two seeds.
204+
LEAK_TEXT = ("Name a planet in the solar system.", None, "Mars is the fourth planet from the Sun.")
205+
LEAK_IMAGE = ("How many objects are in the image?", (168, 280), "I cannot count any distinct objects.")
200206

201207

202208
@pytest.mark.h100
203209
@pytest.mark.megatron
204-
def test_glm5_vlm_packed_vs_alone(glm5_vl_slice):
205-
"""Packed microbatches of 4 (mixed image/text, incl. image-free samples at TP2+SP) must match
206-
each sample run alone. A misplaced image (features of sample k in sample j) or a boundary
207-
leak would make later slots worse than slot 0."""
208-
processor, rows = _build_rows(glm5_vl_slice)
210+
def test_glm5_vlm_no_cross_sample_leak(glm5_vl_slice):
211+
"""A sample's logprobs must not depend on the image content of another sample packed with it.
212+
213+
Packed-vs-alone is not a usable check here: on this truncated slice the MoE forward depends on
214+
the microbatch's shape (per-expert token counts), and packing a sample with a long neighbour
215+
moves it by ~0.13 mean |dlogprob| on the text-only GPTModel path as well. So the microbatch
216+
shape is held fixed and only the neighbour's pixels change: [txt, img] vs [txt, img'] (text
217+
first) and [img, txt] vs [img', txt] (text second), all in one run. Measured noise ~0.01.
218+
"""
219+
processor, (txt,) = _build_rows(glm5_vl_slice, [LEAK_TEXT])
220+
_, (img,) = _build_rows(glm5_vl_slice, [LEAK_IMAGE], seed=0)
221+
_, (img_swapped,) = _build_rows(glm5_vl_slice, [LEAK_IMAGE], seed=1)
222+
assert len(img[0]) == len(img_swapped[0])
223+
# Micro batch 2: microbatches [0,1] [2,3] on DP rank 0, [4,5] [6,7] on DP rank 1.
224+
rows = [txt, img, txt, img_swapped, img, txt, img_swapped, txt]
209225
batch = _batch(processor, rows)
210-
packed = _megatron_logprobs(glm5_vl_slice, batch, micro_batch=MICRO_BATCH)
211-
alone = _megatron_logprobs(glm5_vl_slice, batch, micro_batch=1)
226+
lp = _megatron_logprobs(glm5_vl_slice, batch, micro_batch=2)
212227

213228
scored = batch["loss_mask"].bool()
214-
diff = (packed - alone).abs()
215-
slots = torch.arange(len(rows)) % MICRO_BATCH
216-
for i in range(len(rows)):
217-
print(f" sample {i} slot {slots[i].item()}: |packed-alone|={diff[i][scored[i]].mean().item():.5f}")
218-
assert diff[scored].mean().item() < 2e-2
219-
first, later = slots == 0, slots > 0
220-
assert diff[later][scored[later]].mean().item() <= 3 * diff[first][scored[first]].mean().item() + 1e-2
229+
230+
def mean_diff(a, b):
231+
return (lp[a] - lp[b]).abs()[scored[a]].mean().item()
232+
233+
text_first, text_second = mean_diff(0, 2), mean_diff(5, 7)
234+
image_control = mean_diff(1, 3)
235+
print(
236+
f"\n[glm5 vlm leak] text first {text_first:.4f} text second {text_second:.4f} "
237+
f"image swap control {image_control:.4f}"
238+
)
239+
# The swap must reach the image row itself, or the check below proves nothing.
240+
assert image_control > 0.1
241+
assert text_first < 0.03
242+
assert text_second < 0.03
221243

222244

223245
@pytest.mark.h100

0 commit comments

Comments
 (0)