|
31 | 31 | SLICE = "eatang/GLM-5.3-Flash-4layer" |
32 | 32 | FULL = "zai-org/GLM-5.3-Flash" |
33 | 33 | TP, EP, NUM_GPUS = 2, 4, 4 |
34 | | -MICRO_BATCH = 4 |
35 | 34 |
|
36 | 35 | # (prompt, image size or None, answer). Mixed sizes so the packed stream holds images of |
37 | 36 | # 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 |
82 | 81 | return cfg |
83 | 82 |
|
84 | 83 |
|
85 | | -def _build_rows(model_path: str): |
| 84 | +def _build_rows(model_path: str, prompts=PROMPTS, seed: int = 0): |
86 | 85 | """Per row: (input_ids, answer_len, pixel_values | None, image_grid_thw | None).""" |
87 | 86 | processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True) |
88 | | - gen = torch.Generator().manual_seed(0) |
| 87 | + gen = torch.Generator().manual_seed(seed) |
89 | 88 | rows = [] |
90 | | - for prompt, size, answer in PROMPTS: |
| 89 | + for prompt, size, answer in prompts: |
91 | 90 | content = [{"type": "text", "text": prompt}] |
92 | 91 | images = None |
93 | 92 | if size is not None: |
@@ -148,7 +147,7 @@ def _batch(processor, rows, keep=None) -> TrainingInputBatch: |
148 | 147 | return data |
149 | 148 |
|
150 | 149 |
|
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: |
152 | 151 | """[B, response_length] logprobs from the policy forward (old/ref-logprob path).""" |
153 | 152 | cfg = _config(model_path, language_model_only=language_model_only) |
154 | 153 | cfg.trainer.micro_forward_batch_size_per_gpu = micro_batch |
@@ -194,30 +193,53 @@ def test_glm5_vlm_forward_matches_hf(glm5_vl_slice): |
194 | 193 | diff = (megatron - hf).abs()[scored] |
195 | 194 | print(f"\n[glm5 vlm vs hf] mean={diff.mean().item():.4f} max={diff.max().item():.4f}") |
196 | 195 | # 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.") |
200 | 206 |
|
201 | 207 |
|
202 | 208 | @pytest.mark.h100 |
203 | 209 | @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] |
209 | 225 | 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) |
212 | 227 |
|
213 | 228 | 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 |
221 | 243 |
|
222 | 244 |
|
223 | 245 | @pytest.mark.h100 |
|
0 commit comments