Skip to content

Commit 9db491e

Browse files
committed
[megatron] Accept dtype-string optimizer_config_kwargs (coerce exp_avg_dtype etc. to torch.dtype)
## Summary Megatron's precision-aware `OptimizerConfig` types its `*_dtype` fields (`exp_avg_dtype`, `exp_avg_sq_dtype`, `main_params_dtype`, `params_dtype`) as real `torch.dtype`, but `optimizer_config_kwargs` is forwarded verbatim from YAML/Hydra, which delivers plain strings (e.g. `"bf16"`). Such a string would reach TransformerEngine FusedAdam and crash. This adds a central `str -> torch.dtype` coercion at the single optimizer-construction choke point so low-precision optimizer state can be configured from YAML. ## What changed - New torch-only module `skyrl/backends/skyrl_train/distributed/megatron/optimizer_dtype.py` holding `coerce_optimizer_dtype_kwargs(dict) -> dict` plus two mapping tables: - `_DTYPE_NAME_TO_TORCH`: canonical dtype-name -> `torch.dtype`. The short forms `fp32`/`bf16`/`fp16`/`fp8` follow Megatron-LM's own `dtype_map`; common alias spellings (`bfloat16`, `float16`/`half`, `float32`/`float`, `float8`/`uint8`) are also accepted. `fp8 -> torch.uint8` is a TransformerEngine convention (TE represents FP8 optimizer state as uint8) and is added here, not sourced from Megatron's `dtype_map`. - `_LEGAL_FIELD_DTYPES`: per-field legal sets for the fields actually forwarded to TE FusedAdam — `main_params_dtype` (master weights) restricted to `{fp32, fp16}`; `exp_avg_dtype`/`exp_avg_sq_dtype` allow `{fp32, fp16, bf16, fp8}`. Illegal values raise a clear `ValueError` before reaching FusedAdam. - `optimizer.py`: `init_megatron_optim_config` now calls `coerce_optimizer_dtype_kwargs(optimizer_config_kwargs)` in place of the raw `.update(...)`. The coercion sits at the single shared construction point (sole caller serves SFT and the RL policy). - Docs: documented the new string-dtype support for `optimizer_config_kwargs` in `docs/content/docs/configuration/config.mdx` (next to the existing `use_precision_aware_optimizer` callout, with the full name/alias table) and in `docs/content/docs/examples/megatron.mdx` (concise note cross-linking to the table) — accepted names/aliases, the per-field legal sets, and the `fp8 -> uint8` convention — noting these short forms differ from the full `bfloat16`/`float16`/`float32` spellings accepted by `str_to_torch_dtype` elsewhere. The helper is deliberately kept free of any `megatron.core` import so it can be unit-tested on the CPU CI lane (torch only). Coercion lives at the optimizer-construction choke point rather than `MegatronConfig.__post_init__`, which would replace the YAML strings with `torch.dtype` objects in the dataclass and break the serializable config path (`asdict`/`yaml.dump`). ## Numerical equivalence / safety Byte-identical to current behavior unless a `*_dtype` key is explicitly set in `optimizer_config_kwargs`. With no `*_dtype` overrides the coercion is a pure pass-through copy, so `OptimizerConfig` keeps its existing fp32 defaults for `exp_avg_dtype`/`exp_avg_sq_dtype`/`main_params_dtype` and the hardcoded `params_dtype=torch.bfloat16` seed is unchanged. The default optimizer kwargs contain no `*_dtype` keys, so the default path is unchanged. The only intentional behavior change: a `*_dtype` string that previously would have reached FusedAdam and crashed now becomes the correct `torch.dtype` (enabling low-precision optimizer state), and an illegal `main_params_dtype` now fails fast with a clear message instead of a cryptic TE error. Values already `torch.dtype` and non-dtype kwargs pass through untouched; non-string/non-dtype `*_dtype` values (e.g. `None`) pass through so Megatron's own validation surfaces them. `main_grads_dtype` is coerced str->dtype but intentionally has no legal-set row: at the pinned megatron-core rev it is not forwarded to TE FusedAdam, so there is no TE-backed legal set to enforce; it is left for `OptimizerConfig.__post_init__` to validate (mirroring how `params_dtype` is handled). ## Generality & follow-ups - Covers all Megatron optimizer construction reachable via `init_megatron_optim_config` (SFT and RL policy). The critic worker does not construct an optimizer through this path. - The FSDP optimizer-state path is intentionally out of scope — it is a separate code path with its own mixed-precision config; no change made there. - A separate `str -> torch.dtype` helper already exists (`str_to_torch_dtype` / `PrecisionType.to_dtype`), but neither knows the `fp8 -> uint8` mapping nor does per-field legal-set validation, both of which the precision-aware optimizer-state feature requires; consolidating the canonical name table is a possible follow-up. ## Test plan `tests/backends/skyrl_train/distributed/test_optimizer_dtype_coercion.py`: - `TestCoerceOptimizerDtypeKwargs` (CPU, no skip-guard — runs on the CPU lane): parametrized name->dtype coercion for all aliases, `fp8->uint8`, case/whitespace insensitivity, `torch.dtype` pass-through, `main_params_dtype` accepts fp32/fp16 and rejects bf16/fp8, `params_dtype` coercion, `main_grads_dtype` coerced-but-not-field-validated, unrecognized-name `ValueError`, unrelated kwargs untouched, `None` pass-through, input not mutated. - `TestInitMegatronOptimConfigDtypeCoercion` (megatron-gated via `_has_megatron` skip-guard, no GPU): end-to-end that string kwargs reach a real `OptimizerConfig` with coerced dtypes; `params_dtype` string override replaces the seeded default; default (no override) keeps fp32 defaults; and `use_precision_aware_optimizer=False` + non-fp32 state fast-fails with megatron's own `AssertionError`. Run: ```bash uv run --isolated --extra megatron --extra dev pytest \ tests/backends/skyrl_train/distributed/test_optimizer_dtype_coercion.py -v ``` The CPU class runs on the CPU lane (torch only, megatron-core not required); the megatron-gated class runs wherever megatron-core is installed. No GPU is required by either.
1 parent e1b995f commit 9db491e

5 files changed

Lines changed: 349 additions & 1 deletion

File tree

docs/content/docs/configuration/config.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,34 @@ Some rules for configuring these parameters:
213213
`optimizer_config_kwargs.use_precision_aware_optimizer=true` can cause checkpointing to fail. See: https://github.com/nvidia/megatron-lm/issues/1820. We recommend leaving this setting to `false`.
214214
</Callout>
215215

216+
`OptimizerConfig`'s `*_dtype` fields (`main_params_dtype`, `exp_avg_dtype`, `exp_avg_sq_dtype`, `params_dtype`) are typed as `torch.dtype`, but `optimizer_config_kwargs` is forwarded from YAML as plain strings. We coerce these `*_dtype` strings to `torch.dtype` before constructing `OptimizerConfig`, so low-precision optimizer state can be configured directly from YAML, e.g.:
217+
218+
```yaml
219+
optimizer_config_kwargs:
220+
use_precision_aware_optimizer: true
221+
exp_avg_dtype: bf16
222+
exp_avg_sq_dtype: fp8
223+
main_params_dtype: fp32
224+
```
225+
226+
The accepted dtype-name strings (case- and whitespace-insensitive) are:
227+
228+
| Name | Aliases | `torch.dtype` |
229+
|------|---------|---------------|
230+
| `fp32` | `float32`, `float` | `torch.float32` |
231+
| `fp16` | `float16`, `half` | `torch.float16` |
232+
| `bf16` | `bfloat16` | `torch.bfloat16` |
233+
| `fp8` | `float8`, `uint8` | `torch.uint8` |
234+
235+
`fp8` maps to `torch.uint8` because TransformerEngine represents FP8 optimizer state as `uint8`. Per-field legal sets are enforced before the kwargs reach FusedAdam, so an illegal value fails fast with a clear `ValueError`:
236+
237+
- `main_params_dtype` (master weights): `fp32`, `fp16`
238+
- `exp_avg_dtype` / `exp_avg_sq_dtype`: `fp32`, `fp16`, `bf16`, `fp8`
239+
240+
<Callout type="info">
241+
These short forms are specific to `optimizer_config_kwargs` and differ from the full `bfloat16` / `float16` / `float32` spellings accepted by `str_to_torch_dtype` used elsewhere in SkyRL.
242+
</Callout>
243+
216244
## Optimizer Configuration
217245

218246
For both the critic and policy model, we provide a common optimizer configuration

docs/content/docs/examples/megatron.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ empty_cuda_cache: true
8585
8686
These default values can be overridden by passing in the corresponding arguments to `trainer.policy.megatron_config` in the launch script.
8787
88+
`optimizer_config_kwargs` accepts dtype-name strings for its `*_dtype` fields (`main_params_dtype`, `exp_avg_dtype`, `exp_avg_sq_dtype`, `params_dtype`), which are coerced to `torch.dtype` before constructing `OptimizerConfig`. This lets you configure low-precision optimizer state from YAML:
89+
90+
```yaml
91+
optimizer_config_kwargs:
92+
use_precision_aware_optimizer: true
93+
exp_avg_dtype: bf16
94+
exp_avg_sq_dtype: fp8
95+
main_params_dtype: fp32
96+
```
97+
98+
The accepted short forms are `fp32` (aliases `float32`/`float`), `fp16` (`float16`/`half`), `bf16` (`bfloat16`), and `fp8` (`float8`/`uint8`); `fp8` maps to `torch.uint8` since TransformerEngine represents FP8 optimizer state as `uint8`. `main_params_dtype` is restricted to `fp32`/`fp16`, while `exp_avg_dtype`/`exp_avg_sq_dtype` additionally allow `bf16`/`fp8`; illegal values fail fast with a `ValueError`. See the [Megatron configuration guide](../configuration/config#megatron-configuration) for the full table.
99+
88100
## Parallelism Resources
89101
90102
Understanding and configuring parallelism strategies for large models can be challenging.

skyrl/backends/skyrl_train/distributed/megatron/optimizer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
from megatron.core.optimizer_param_scheduler import OptimizerParamScheduler
2727
from omegaconf import DictConfig
2828

29+
from skyrl.backends.skyrl_train.distributed.megatron.optimizer_dtype import (
30+
coerce_optimizer_dtype_kwargs,
31+
)
2932
from skyrl.train.config import OptimizerConfig as SkyRLOptimizerConfig
3033

3134

@@ -45,7 +48,9 @@ def init_megatron_optim_config(
4548
"params_dtype": torch.bfloat16,
4649
"use_distributed_optimizer": True,
4750
}
48-
optim_args.update(optimizer_config_kwargs)
51+
# Coerce any ``*_dtype`` string (e.g. "bf16" from YAML) into a real torch.dtype
52+
# before it reaches Megatron's OptimizerConfig / FusedAdam, which require dtypes.
53+
optim_args.update(coerce_optimizer_dtype_kwargs(optimizer_config_kwargs))
4954

5055
config = OptimizerConfig(**optim_args)
5156
return config
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Pure-Python (torch-only) coercion of Megatron optimizer ``*_dtype`` kwargs.
2+
3+
This is intentionally kept free of any ``megatron.core`` import so the coercion
4+
logic can be unit-tested on the cheap CPU CI lane (which installs torch but not
5+
megatron-core). ``optimizer.py`` imports the public helper from here.
6+
"""
7+
8+
from typing import Any, Dict, Set
9+
10+
import torch
11+
12+
# Canonical dtype-name -> torch.dtype mapping, mirroring Megatron-LM's own
13+
# ``dtype_map`` in ``megatron/training/arguments.py`` (the short forms 'fp32',
14+
# 'bf16', 'fp16', 'fp8' are the ones Megatron itself maps). The extra aliases
15+
# ('bfloat16', 'float16'/'half', 'float32'/'float', 'float8'/'uint8') accept the
16+
# spellings a user might reasonably write in YAML. ``fp8`` maps to ``torch.uint8``
17+
# because TransformerEngine represents FP8 optimizer state as uint8.
18+
_DTYPE_NAME_TO_TORCH: Dict[str, torch.dtype] = {
19+
"fp32": torch.float32,
20+
"float32": torch.float32,
21+
"float": torch.float32,
22+
"bf16": torch.bfloat16,
23+
"bfloat16": torch.bfloat16,
24+
"fp16": torch.float16,
25+
"float16": torch.float16,
26+
"half": torch.float16,
27+
"fp8": torch.uint8,
28+
"float8": torch.uint8,
29+
"uint8": torch.uint8,
30+
}
31+
32+
# Per-field legal dtypes, enforced before the kwargs reach FusedAdam (which would
33+
# otherwise raise a cryptic error deep inside TransformerEngine). Only fields that
34+
# are actually forwarded to TE FusedAdam — and whose accepted set is verifiable in
35+
# the TE source — are listed here. ``main_params_dtype`` (a.k.a. master weights)
36+
# maps to FusedAdam's ``master_weight_dtype``, which only supports fp32/fp16;
37+
# ``exp_avg_dtype`` / ``exp_avg_sq_dtype`` additionally allow bf16/fp8.
38+
# ``main_grads_dtype`` is deliberately NOT listed: at the pinned megatron-core rev
39+
# it is not forwarded to FusedAdam (see megatron/core/optimizer/__init__.py, which
40+
# only passes exp_avg_dtype, exp_avg_sq_dtype, and main_params_dtype as
41+
# master_weight_dtype), so there is no TE-backed legal set to enforce — it is still
42+
# coerced str->dtype here and left for ``OptimizerConfig.__post_init__`` to validate
43+
# (mirroring how ``params_dtype`` is handled: coerced, not field-validated).
44+
# Fields not listed here accept any value in ``_DTYPE_NAME_TO_TORCH``.
45+
_LEGAL_FIELD_DTYPES: Dict[str, Set[torch.dtype]] = {
46+
"main_params_dtype": {torch.float32, torch.float16},
47+
"exp_avg_dtype": {torch.float32, torch.bfloat16, torch.float16, torch.uint8},
48+
"exp_avg_sq_dtype": {torch.float32, torch.bfloat16, torch.float16, torch.uint8},
49+
}
50+
51+
52+
def coerce_optimizer_dtype_kwargs(optimizer_config_kwargs: Dict[str, Any]) -> Dict[str, Any]:
53+
"""Coerce ``*_dtype`` string values in Megatron optimizer kwargs to ``torch.dtype``.
54+
55+
Megatron's precision-aware ``OptimizerConfig`` types ``exp_avg_dtype`` /
56+
``exp_avg_sq_dtype`` / ``main_params_dtype`` (and friends) as real ``torch.dtype``,
57+
but SkyRL forwards ``optimizer_config_kwargs`` verbatim from YAML/Hydra, which
58+
delivers plain strings (e.g. ``"bf16"``). This converts any ``*_dtype`` key whose
59+
value is a dtype-name string into the corresponding ``torch.dtype`` using Megatron-LM's
60+
canonical mapping, validates the result against the per-field legal set, and leaves
61+
everything else (non-``*_dtype`` keys, values already ``torch.dtype``) untouched.
62+
63+
Returns a new dict; the input is not mutated.
64+
65+
Raises:
66+
ValueError: if a ``*_dtype`` value is an unrecognized dtype name, or if a coerced
67+
dtype is illegal for that specific field (e.g. bf16/fp8 for ``main_params_dtype``).
68+
"""
69+
coerced: Dict[str, Any] = {}
70+
for key, value in optimizer_config_kwargs.items():
71+
if not key.endswith("_dtype"):
72+
coerced[key] = value
73+
continue
74+
75+
if isinstance(value, torch.dtype):
76+
dtype = value
77+
elif isinstance(value, str):
78+
name = value.strip().lower()
79+
if name not in _DTYPE_NAME_TO_TORCH:
80+
raise ValueError(
81+
f"Unrecognized dtype name {value!r} for optimizer kwarg {key!r}. "
82+
f"Expected one of {sorted(_DTYPE_NAME_TO_TORCH)} or a torch.dtype."
83+
)
84+
dtype = _DTYPE_NAME_TO_TORCH[name]
85+
else:
86+
# Not a dtype-name string or torch.dtype (e.g. None); pass through so
87+
# Megatron's own validation surfaces any problem.
88+
coerced[key] = value
89+
continue
90+
91+
legal = _LEGAL_FIELD_DTYPES.get(key)
92+
if legal is not None and dtype not in legal:
93+
legal_names = sorted({n for n, d in _DTYPE_NAME_TO_TORCH.items() if d in legal})
94+
raise ValueError(f"Illegal dtype {dtype} for optimizer kwarg {key!r}; legal values are {legal_names}.")
95+
coerced[key] = dtype
96+
return coerced
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""CPU tests for central Megatron optimizer-state dtype coercion.
2+
3+
Verifies that ``*_dtype`` string values forwarded through
4+
``MegatronConfig.optimizer_config_kwargs`` (e.g. "bf16" from YAML/Hydra) are
5+
coerced to real ``torch.dtype`` before reaching Megatron's precision-aware
6+
``OptimizerConfig`` / TransformerEngine FusedAdam, using Megatron-LM's own
7+
canonical dtype mapping; that illegal values for ``main_params_dtype`` (master
8+
weights, fp32/fp16 only) are rejected; and that unrelated kwargs pass through
9+
untouched.
10+
11+
``TestCoerceOptimizerDtypeKwargs`` exercises the pure-Python (torch-only)
12+
coercion helper, which lives in a module that does NOT import megatron-core, so
13+
it runs unconditionally on the cheap CPU CI lane (which installs torch but not
14+
megatron-core). ``TestInitMegatronOptimConfigDtypeCoercion`` builds a real
15+
``OptimizerConfig`` and is therefore skipped when megatron-core is not installed
16+
(mirroring ``test_megatron_correctness.py``). No CUDA is required by either.
17+
18+
uv run --isolated --extra megatron --extra dev pytest \
19+
tests/backends/skyrl_train/distributed/test_optimizer_dtype_coercion.py -v
20+
"""
21+
22+
import sys
23+
24+
import pytest
25+
import torch
26+
27+
from skyrl.backends.skyrl_train.distributed.megatron.optimizer_dtype import (
28+
coerce_optimizer_dtype_kwargs,
29+
)
30+
31+
_has_megatron = "megatron" in sys.modules or __import__("importlib").util.find_spec("megatron") is not None
32+
33+
34+
class TestCoerceOptimizerDtypeKwargs:
35+
"""``coerce_optimizer_dtype_kwargs`` maps dtype-name strings to torch.dtype.
36+
37+
Runs unconditionally: the helper module is torch-only (no megatron-core).
38+
"""
39+
40+
def _coerce(self, kwargs: dict) -> dict:
41+
return coerce_optimizer_dtype_kwargs(kwargs)
42+
43+
@pytest.mark.parametrize(
44+
"name,expected",
45+
[
46+
("bf16", torch.bfloat16),
47+
("bfloat16", torch.bfloat16),
48+
("fp16", torch.float16),
49+
("float16", torch.float16),
50+
("half", torch.float16),
51+
("fp32", torch.float32),
52+
("float32", torch.float32),
53+
("float", torch.float32),
54+
("fp8", torch.uint8),
55+
("float8", torch.uint8),
56+
("uint8", torch.uint8),
57+
],
58+
)
59+
def test_string_names_coerce_to_torch_dtype(self, name, expected):
60+
"""Each canonical/alias dtype name maps to the right torch.dtype."""
61+
# exp_avg_dtype legally accepts fp32/fp16/bf16/fp8, so it can exercise all names.
62+
out = self._coerce({"exp_avg_dtype": name})
63+
assert out["exp_avg_dtype"] == expected
64+
assert isinstance(out["exp_avg_dtype"], torch.dtype)
65+
66+
def test_fp8_maps_to_uint8(self):
67+
"""TE represents fp8 optimizer state as uint8."""
68+
out = self._coerce({"exp_avg_sq_dtype": "fp8"})
69+
assert out["exp_avg_sq_dtype"] is torch.uint8
70+
71+
def test_case_and_whitespace_insensitive(self):
72+
out = self._coerce({"exp_avg_dtype": " BF16 "})
73+
assert out["exp_avg_dtype"] is torch.bfloat16
74+
75+
def test_already_torch_dtype_passes_through(self):
76+
"""A value already a torch.dtype is preserved as-is."""
77+
out = self._coerce({"exp_avg_dtype": torch.bfloat16})
78+
assert out["exp_avg_dtype"] is torch.bfloat16
79+
80+
def test_main_params_dtype_accepts_fp32_and_fp16(self):
81+
"""main_params_dtype (master weights) legally accepts only fp32/fp16."""
82+
assert self._coerce({"main_params_dtype": "fp32"})["main_params_dtype"] is torch.float32
83+
assert self._coerce({"main_params_dtype": "fp16"})["main_params_dtype"] is torch.float16
84+
85+
@pytest.mark.parametrize("bad", ["bf16", "fp8"])
86+
def test_main_params_dtype_rejects_bf16_and_fp8(self, bad):
87+
"""bf16/fp8 are illegal master-weight dtypes and must raise."""
88+
with pytest.raises(ValueError, match="main_params_dtype"):
89+
self._coerce({"main_params_dtype": bad})
90+
91+
@pytest.mark.parametrize(
92+
"name,expected", [("bf16", torch.bfloat16), ("fp16", torch.float16), ("fp32", torch.float32)]
93+
)
94+
def test_params_dtype_is_coerced_with_no_field_restriction(self, name, expected):
95+
"""``params_dtype`` ends in ``_dtype`` and has no legal-set entry, so it is
96+
coerced for any recognized alias and overrides the bf16 default that
97+
``init_megatron_optim_config`` seeds (see optimizer.py)."""
98+
out = self._coerce({"params_dtype": name})
99+
assert out["params_dtype"] is expected
100+
101+
def test_main_grads_dtype_coerced_but_not_field_validated(self):
102+
"""``main_grads_dtype`` is not forwarded to FusedAdam at the pinned rev, so it
103+
has no legal-set row: it is coerced str->dtype but any value is accepted here,
104+
leaving megatron-core's ``__post_init__`` to validate it. bf16 (which a legal
105+
set would reject) coerces fine."""
106+
out = self._coerce({"main_grads_dtype": "bf16"})
107+
assert out["main_grads_dtype"] is torch.bfloat16
108+
109+
def test_unrecognized_dtype_name_raises(self):
110+
with pytest.raises(ValueError, match="Unrecognized dtype name"):
111+
self._coerce({"exp_avg_dtype": "bf17"})
112+
113+
def test_unrelated_kwargs_pass_through_untouched(self):
114+
"""Non-``*_dtype`` keys are returned unchanged."""
115+
kwargs = {
116+
"use_precision_aware_optimizer": True,
117+
"optimizer_offload_fraction": 0.5,
118+
"overlap_cpu_optimizer_d2h_h2d": False,
119+
"exp_avg_dtype": "bf16",
120+
}
121+
out = self._coerce(kwargs)
122+
assert out["use_precision_aware_optimizer"] is True
123+
assert out["optimizer_offload_fraction"] == 0.5
124+
assert out["overlap_cpu_optimizer_d2h_h2d"] is False
125+
assert out["exp_avg_dtype"] is torch.bfloat16
126+
127+
def test_non_string_non_dtype_dtype_value_passes_through(self):
128+
"""A ``*_dtype`` key whose value is neither a dtype name nor torch.dtype
129+
(e.g. None) passes through so Megatron's own validation surfaces it."""
130+
out = self._coerce({"main_grads_dtype": None})
131+
assert out["main_grads_dtype"] is None
132+
133+
def test_input_not_mutated(self):
134+
"""The helper returns a new dict and does not mutate the input."""
135+
kwargs = {"exp_avg_dtype": "bf16"}
136+
self._coerce(kwargs)
137+
assert kwargs["exp_avg_dtype"] == "bf16"
138+
139+
140+
@pytest.mark.skipif(not _has_megatron, reason="megatron-core not installed")
141+
class TestInitMegatronOptimConfigDtypeCoercion:
142+
"""End-to-end: ``init_megatron_optim_config`` builds a real OptimizerConfig
143+
with coerced dtypes from string kwargs."""
144+
145+
def test_string_dtype_kwargs_reach_optimizer_config(self):
146+
from skyrl.backends.skyrl_train.distributed.megatron.optimizer import (
147+
init_megatron_optim_config,
148+
)
149+
from skyrl.train.config import OptimizerConfig as SkyRLOptimizerConfig
150+
151+
optim_config = SkyRLOptimizerConfig()
152+
config = init_megatron_optim_config(
153+
optim_config,
154+
{
155+
"use_precision_aware_optimizer": True,
156+
"exp_avg_dtype": "bf16",
157+
"exp_avg_sq_dtype": "fp8",
158+
"main_params_dtype": "fp32",
159+
},
160+
)
161+
assert config.exp_avg_dtype is torch.bfloat16
162+
assert config.exp_avg_sq_dtype is torch.uint8
163+
assert config.main_params_dtype is torch.float32
164+
165+
def test_params_dtype_string_override_reaches_optimizer_config(self):
166+
"""A string ``params_dtype`` override is coerced and replaces the seeded
167+
bf16 default in the constructed OptimizerConfig."""
168+
from skyrl.backends.skyrl_train.distributed.megatron.optimizer import (
169+
init_megatron_optim_config,
170+
)
171+
from skyrl.train.config import OptimizerConfig as SkyRLOptimizerConfig
172+
173+
config = init_megatron_optim_config(SkyRLOptimizerConfig(), {"params_dtype": "fp16"})
174+
assert config.params_dtype is torch.float16
175+
176+
def test_default_kwargs_leave_dtypes_at_megatron_defaults(self):
177+
"""With no ``*_dtype`` overrides, OptimizerConfig keeps its fp32 defaults
178+
(byte-identical to prior behavior)."""
179+
from skyrl.backends.skyrl_train.distributed.megatron.optimizer import (
180+
init_megatron_optim_config,
181+
)
182+
from skyrl.train.config import OptimizerConfig as SkyRLOptimizerConfig
183+
184+
config = init_megatron_optim_config(SkyRLOptimizerConfig(), {})
185+
assert config.exp_avg_dtype is torch.float32
186+
assert config.exp_avg_sq_dtype is torch.float32
187+
assert config.main_params_dtype is torch.float32
188+
189+
def test_precision_aware_off_with_nonfp32_state_fast_fails_in_megatron(self):
190+
"""Coercing ``exp_avg_dtype='bf16'`` passes the helper's own validation, but
191+
megatron-core's ``OptimizerConfig.__post_init__`` then asserts that
192+
exp_avg_dtype can only be fp32 when ``use_precision_aware_optimizer`` is False.
193+
194+
This documents that the fast-fail is megatron's (a real AssertionError),
195+
not a silent mis-coercion: the helper coerces the string fine, the rejection
196+
happens downstream in OptimizerConfig construction.
197+
"""
198+
from skyrl.backends.skyrl_train.distributed.megatron.optimizer import (
199+
init_megatron_optim_config,
200+
)
201+
from skyrl.train.config import OptimizerConfig as SkyRLOptimizerConfig
202+
203+
with pytest.raises(AssertionError, match="exp_avg_dtype can only be fp32"):
204+
init_megatron_optim_config(
205+
SkyRLOptimizerConfig(),
206+
{"use_precision_aware_optimizer": False, "exp_avg_dtype": "bf16"},
207+
)

0 commit comments

Comments
 (0)