Skip to content

Commit de1527a

Browse files
authored
fix(config): reject non-positive numeric training-config fields at load (#175)
1 parent 4687388 commit de1527a

3 files changed

Lines changed: 127 additions & 2 deletions

File tree

tests/test_training_config_validation.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
"""Regression tests for training batch-size validation at config load.
1+
"""Regression tests for training-config validation at config load.
22
33
Guards the empty-dispatch NCCL-hang root cause behind the issue #126 surface:
44
a ``training.micro_batch_size`` of 0 makes the derived ``dispatch_batch_size`` 0,
55
so ``try_dispatch_batch`` no-op-dispatches (returns True while queuing nothing)
66
and every rank blocks on the data-fetcher queue, surfacing as an NCCL
77
all-gather timeout. The validator rejects non-positive sizes at config load,
88
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.
917
"""
1018

1119
import pytest
@@ -57,3 +65,82 @@ def test_validate_training_batch_config_accepts_positive():
5765

5866
config = _resolved_config(micro_batch_size=8)
5967
_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)

torchspec/config/inference_config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from dataclasses import dataclass, field
3030
from typing import Any, Dict, Optional
3131

32+
from omegaconf import DictConfig
33+
3234
from torchspec.config.mooncake_config import MooncakeConfig
3335

3436

@@ -198,3 +200,12 @@ class HFInferenceConfig:
198200
trust_remote_code: bool = False
199201
aux_hidden_states_layers: Optional[list[int]] = None
200202
mooncake_config: Optional[MooncakeConfig] = None
203+
204+
205+
def _validate_inference_batch_config(config: DictConfig) -> None:
206+
if config.inference.inference_batch_size <= 0:
207+
raise ValueError(
208+
f"inference_batch_size must be > 0 (got {config.inference.inference_batch_size}); "
209+
f"0 causes inference-pool starvation (silent dispatch spin) and vLLM rejects "
210+
f"max_num_seqs=0 late after init"
211+
)

torchspec/config/train_config.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from omegaconf import DictConfig, OmegaConf
2828

29-
from torchspec.config.inference_config import InferenceConfig
29+
from torchspec.config.inference_config import InferenceConfig, _validate_inference_batch_config
3030
from torchspec.data.utils import is_local_data_path
3131
from torchspec.utils.logging import logger
3232

@@ -300,6 +300,31 @@ def _validate_training_batch_config(config: DictConfig) -> None:
300300
)
301301

302302

303+
def _validate_training_numeric_config(config: DictConfig) -> None:
304+
"""Reject non-positive values that would otherwise fail silently or crash late.
305+
306+
These fields share the fail-closed-at-load principle of #171: a misconfig that
307+
produces flat loss, sign-flipped gradients, or an opaque post-init crash is
308+
preferable to surfacing after expensive Ray/mooncake init.
309+
"""
310+
if config.training.draft_accumulation_steps <= 0:
311+
raise ValueError(
312+
f"draft_accumulation_steps must be > 0 (got {config.training.draft_accumulation_steps}); "
313+
f"<=0 propagates into global_batch_size/lr_total_steps and crashes post-init "
314+
f"(ZeroDivisionError or bare total_steps assert)"
315+
)
316+
if config.training.learning_rate <= 0:
317+
raise ValueError(
318+
f"learning_rate must be > 0 (got {config.training.learning_rate}); "
319+
f"0 yields silent flat loss (untrained checkpoint), <0 hits a late assert"
320+
)
321+
if config.training.max_grad_norm <= 0:
322+
raise ValueError(
323+
f"max_grad_norm must be > 0 (got {config.training.max_grad_norm}); "
324+
f"0 zeroes all grads (silent flat loss), <0 sign-flips grads (silent gradient ascent)"
325+
)
326+
327+
303328
def _save_config_snapshot(config: DictConfig) -> None:
304329
"""Save the resolved config to output_dir/config.yaml if output_dir is set."""
305330
output_dir = OmegaConf.select(config, "output_dir", default=None)
@@ -347,6 +372,8 @@ def load_config(
347372
_validate_offline_config(config)
348373
_validate_vocab_mapping_config(config)
349374
_validate_training_batch_config(config)
375+
_validate_training_numeric_config(config)
376+
_validate_inference_batch_config(config)
350377

351378
if save_snapshot:
352379
_save_config_snapshot(config)

0 commit comments

Comments
 (0)