|
1 | | -"""Regression tests for training batch-size validation at config load. |
| 1 | +"""Regression tests for training-config validation at config load. |
2 | 2 |
|
3 | 3 | Guards the empty-dispatch NCCL-hang root cause behind the issue #126 surface: |
4 | 4 | a ``training.micro_batch_size`` of 0 makes the derived ``dispatch_batch_size`` 0, |
5 | 5 | so ``try_dispatch_batch`` no-op-dispatches (returns True while queuing nothing) |
6 | 6 | and every rank blocks on the data-fetcher queue, surfacing as an NCCL |
7 | 7 | all-gather timeout. The validator rejects non-positive sizes at config load, |
8 | 8 | before any training process starts. |
| 9 | +
|
| 10 | +Extends the same fail-closed-at-load idiom (PR #171) to four more numeric |
| 11 | +fields whose non-positive values currently fail *silently* (flat loss from |
| 12 | +``learning_rate=0`` / ``max_grad_norm=0``, sign-flipped gradients from |
| 13 | +``max_grad_norm<0``, inference-pool starvation from ``inference_batch_size=0``) |
| 14 | +or crash *opaquely and late* (post-init ``ZeroDivisionError`` / bare |
| 15 | +``total_steps`` assert from ``draft_accumulation_steps<=0``). Rejecting at |
| 16 | +load surfaces the misconfig before Ray/mooncake/vLLM init. |
9 | 17 | """ |
10 | 18 |
|
11 | 19 | import pytest |
@@ -57,3 +65,82 @@ def test_validate_training_batch_config_accepts_positive(): |
57 | 65 |
|
58 | 66 | config = _resolved_config(micro_batch_size=8) |
59 | 67 | _validate_training_batch_config(config) # must not raise |
| 68 | + |
| 69 | + |
| 70 | +# --- Numeric training-config fields (draft_accumulation_steps, learning_rate, |
| 71 | +# max_grad_norm) and inference_batch_size — PR #171 idiom extended. --- |
| 72 | + |
| 73 | + |
| 74 | +def _resolved_training_config(**overrides): |
| 75 | + """Build a fully-resolved config from the schema defaults + training overrides.""" |
| 76 | + config = OmegaConf.structured(Config) |
| 77 | + for key, value in overrides.items(): |
| 78 | + setattr(config.training, key, value) |
| 79 | + return config |
| 80 | + |
| 81 | + |
| 82 | +def _resolved_inference_config(inference_batch_size: int): |
| 83 | + """Build a fully-resolved config from the schema defaults + an inference-batch override.""" |
| 84 | + config = OmegaConf.structured(Config) |
| 85 | + config.inference.inference_batch_size = inference_batch_size |
| 86 | + return config |
| 87 | + |
| 88 | + |
| 89 | +def test_load_config_rejects_zero_draft_accumulation_steps(): |
| 90 | + """draft_accumulation_steps=0 must fail at load: propagates to global_batch_size=0 |
| 91 | + and crashes post-init (ZeroDivisionError or bare total_steps assert).""" |
| 92 | + base = _resolved_training_config(draft_accumulation_steps=0) |
| 93 | + with pytest.raises(ValueError, match="draft_accumulation_steps"): |
| 94 | + load_config(base_config=base) |
| 95 | + |
| 96 | + |
| 97 | +def test_load_config_rejects_negative_draft_accumulation_steps(): |
| 98 | + """Negative values propagate the same way and must be rejected at load.""" |
| 99 | + base = _resolved_training_config(draft_accumulation_steps=-2) |
| 100 | + with pytest.raises(ValueError, match="draft_accumulation_steps"): |
| 101 | + load_config(base_config=base) |
| 102 | + |
| 103 | + |
| 104 | +def test_load_config_rejects_zero_learning_rate(): |
| 105 | + """learning_rate=0 yields silent flat loss (AdamW+scheduler at lr=0) and an |
| 106 | + untrained checkpoint; reject at load.""" |
| 107 | + base = _resolved_training_config(learning_rate=0) |
| 108 | + with pytest.raises(ValueError, match="learning_rate"): |
| 109 | + load_config(base_config=base) |
| 110 | + |
| 111 | + |
| 112 | +def test_load_config_rejects_negative_learning_rate(): |
| 113 | + """Negative learning rates hit a late scheduler assert; reject at load.""" |
| 114 | + base = _resolved_training_config(learning_rate=-1e-4) |
| 115 | + with pytest.raises(ValueError, match="learning_rate"): |
| 116 | + load_config(base_config=base) |
| 117 | + |
| 118 | + |
| 119 | +def test_load_config_rejects_zero_max_grad_norm(): |
| 120 | + """max_grad_norm=0 zeroes all grads via clip_grad_norm_ (silent flat loss); |
| 121 | + reject at load.""" |
| 122 | + base = _resolved_training_config(max_grad_norm=0) |
| 123 | + with pytest.raises(ValueError, match="max_grad_norm"): |
| 124 | + load_config(base_config=base) |
| 125 | + |
| 126 | + |
| 127 | +def test_load_config_rejects_negative_max_grad_norm(): |
| 128 | + """Negative max_grad_norm sign-flips grads (silent gradient ascent); reject at load.""" |
| 129 | + base = _resolved_training_config(max_grad_norm=-0.5) |
| 130 | + with pytest.raises(ValueError, match="max_grad_norm"): |
| 131 | + load_config(base_config=base) |
| 132 | + |
| 133 | + |
| 134 | +def test_load_config_rejects_zero_inference_batch_size(): |
| 135 | + """inference_batch_size=0 starves the inference pool (silent dispatch spin) and |
| 136 | + vLLM rejects max_num_seqs=0 late after init; reject at load.""" |
| 137 | + base = _resolved_inference_config(inference_batch_size=0) |
| 138 | + with pytest.raises(ValueError, match="inference_batch_size"): |
| 139 | + load_config(base_config=base) |
| 140 | + |
| 141 | + |
| 142 | +def test_load_config_rejects_negative_inference_batch_size(): |
| 143 | + """Negative inference batch sizes starve the pool the same way; reject at load.""" |
| 144 | + base = _resolved_inference_config(inference_batch_size=-1) |
| 145 | + with pytest.raises(ValueError, match="inference_batch_size"): |
| 146 | + load_config(base_config=base) |
0 commit comments