Skip to content

Commit 18d5a4c

Browse files
committed
Add checkpoint conversion test
Signed-off-by: Ao Tang <aot@nvidia.com>
1 parent e97057a commit 18d5a4c

4 files changed

Lines changed: 196 additions & 6 deletions

File tree

‎src/megatron/bridge/diffusion/conversion/flux/flux_bridge.py‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Dict, Mapping
15+
from typing import Dict, List, Mapping
1616

1717
import torch
1818
from diffusers import FluxTransformer2DModel
@@ -29,6 +29,23 @@
2929
)
3030

3131

32+
def _flux_axes_dims_rope(hf_config) -> List[int]:
33+
"""Normalize diffusers tuple/list config to ``List[int]`` for :class:`FluxProvider`."""
34+
raw = hf_config.axes_dims_rope
35+
if isinstance(raw, (list, tuple)):
36+
return [int(x) for x in raw]
37+
raise TypeError(f"Unexpected axes_dims_rope type: {type(raw)}")
38+
39+
40+
def _flux_ffn_hidden_size(hf_config, hidden_size: int) -> int:
41+
"""Match HF / diffusers FFN width to Megatron ``ffn_hidden_size``."""
42+
explicit = getattr(hf_config, "ffn_dim", None) or getattr(hf_config, "intermediate_size", None)
43+
if explicit is not None:
44+
return int(explicit)
45+
mlp_ratio = getattr(hf_config, "mlp_ratio", 4)
46+
return int(mlp_ratio * hidden_size)
47+
48+
3249
@MegatronModelBridge.register_bridge(source=FluxTransformer2DModel, target=Flux)
3350
class FluxBridge(MegatronModelBridge):
3451
"""
@@ -45,19 +62,24 @@ class FluxBridge(MegatronModelBridge):
4562
def provider_bridge(self, hf_pretrained: PreTrainedFlux) -> FluxProvider:
4663
hf_config = hf_pretrained.config
4764

65+
# Diffusers FLUX width is heads * per-head dim; defaults were wrong if we only set attention fields.
66+
hidden_size = hf_config.num_attention_heads * hf_config.attention_head_dim
67+
ffn_hidden_size = _flux_ffn_hidden_size(hf_config, hidden_size)
68+
4869
provider = FluxProvider(
4970
in_channels=hf_config.in_channels,
5071
patch_size=hf_config.patch_size,
5172
num_joint_layers=hf_config.num_layers,
5273
num_single_layers=hf_config.num_single_layers,
5374
num_attention_heads=hf_config.num_attention_heads,
54-
# out_channels: None
55-
# joint_attention_dim: 4096
75+
hidden_size=hidden_size,
76+
ffn_hidden_size=ffn_hidden_size,
77+
context_dim=hf_config.joint_attention_dim,
5678
kv_channels=hf_config.attention_head_dim,
5779
num_query_groups=hf_config.num_attention_heads,
5880
vec_in_dim=hf_config.pooled_projection_dim,
5981
guidance_embed=hf_config.guidance_embeds,
60-
axes_dims_rope=hf_config.axes_dims_rope,
82+
axes_dims_rope=_flux_axes_dims_rope(hf_config),
6183
bf16=False,
6284
params_dtype=torch.float32,
6385
)

‎tests/functional_tests/L2_Launch_models_flux.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ export CUDA_VISIBLE_DEVICES="0,1"
1919

2020
uv run coverage run --data-file=/opt/Megatron-Bridge/.coverage --source=/opt/Megatron-Bridge/ --parallel-mode -m pytest \
2121
-o log_cli=true -o log_cli_level=INFO -v -s -x -m "not pleasefixme" --tb=short -rA \
22-
tests/functional_tests/diffusion/flux/test_flux_pretrain.py
22+
tests/functional_tests/diffusion/flux/test_flux_pretrain.py \
23+
tests/functional_tests/diffusion/flux/test_flux_conversion.py
2324
coverage combine -q
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Functional tests for FLUX HF <-> Megatron checkpoint conversion.
17+
18+
Uses a tiny randomly initialized diffusers FluxTransformer2DModel saved under a local
19+
``transformer/`` layout (same as FLUX.1-dev) and drives
20+
``examples/diffusion/recipes/flux/conversion/convert_checkpoints.py``.
21+
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import subprocess
27+
from pathlib import Path
28+
29+
import pytest
30+
import torch
31+
32+
33+
diffusers = pytest.importorskip("diffusers")
34+
FluxTransformer2DModel = diffusers.FluxTransformer2DModel
35+
36+
# Repo root: tests/functional_tests/diffusion/flux -> five parents
37+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent.parent
38+
CONVERT_SCRIPT = "examples/diffusion/recipes/flux/conversion/convert_checkpoints.py"
39+
40+
41+
def _build_toy_flux_hf_root(base: Path) -> Path:
42+
"""
43+
Create ``base`` as a FLUX-style HF root with weights under ``base/transformer/``.
44+
45+
Dimensions are chosen so ``sum(axes_dims_rope) == attention_head_dim`` (128), matching
46+
real FLUX layouts, while keeping layers minimal for speed.
47+
"""
48+
hf_root = base / "flux_toy_hf"
49+
transformer_dir = hf_root / "transformer"
50+
transformer_dir.mkdir(parents=True, exist_ok=True)
51+
52+
model = FluxTransformer2DModel(
53+
patch_size=1,
54+
in_channels=64,
55+
num_layers=1,
56+
num_single_layers=2,
57+
attention_head_dim=128,
58+
num_attention_heads=8,
59+
joint_attention_dim=4096,
60+
pooled_projection_dim=768,
61+
guidance_embeds=False,
62+
axes_dims_rope=(16, 56, 56),
63+
)
64+
model = model.to(dtype=torch.bfloat16)
65+
model.save_pretrained(transformer_dir, safe_serialization=True)
66+
return hf_root
67+
68+
69+
class TestFluxCheckpointConversion:
70+
"""HF FLUX transformer (diffusers) <-> Megatron via convert_checkpoints.py."""
71+
72+
@pytest.fixture(scope="class")
73+
def flux_toy_hf_root(self, tmp_path_factory: pytest.TempPathFactory) -> Path:
74+
base = tmp_path_factory.mktemp("flux_conv_toy")
75+
return _build_toy_flux_hf_root(Path(base))
76+
77+
@pytest.fixture(scope="class")
78+
def flux_megatron_ckpt_dir(self, flux_toy_hf_root: Path, tmp_path_factory: pytest.TempPathFactory) -> Path:
79+
"""
80+
Run ``convert_checkpoints.py import`` once for the class.
81+
82+
Export test no longer repeats this (~10–30s+ per run depending on GPU).
83+
"""
84+
megatron_out = Path(tmp_path_factory.mktemp("flux_megatron_shared"))
85+
megatron_out.mkdir(parents=True, exist_ok=True)
86+
cmd = [
87+
"python",
88+
"-m",
89+
"coverage",
90+
"run",
91+
"--data-file=/opt/Megatron-Bridge/.coverage",
92+
"--source=/opt/Megatron-Bridge/",
93+
"--parallel-mode",
94+
CONVERT_SCRIPT,
95+
"import",
96+
"--hf-model",
97+
str(flux_toy_hf_root),
98+
"--megatron-path",
99+
str(megatron_out),
100+
"--torch-dtype",
101+
"bfloat16",
102+
]
103+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO_ROOT)
104+
if result.returncode != 0:
105+
print("STDOUT:", result.stdout)
106+
print("STDERR:", result.stderr)
107+
pytest.fail(f"FLUX import (class fixture) failed with exit code {result.returncode}")
108+
assert "Successfully imported model to:" in result.stdout
109+
assert any(megatron_out.iterdir()), f"Megatron output empty: {megatron_out}"
110+
return megatron_out
111+
112+
def test_toy_flux_layout(self, flux_toy_hf_root: Path) -> None:
113+
tdir = flux_toy_hf_root / "transformer"
114+
assert tdir.is_dir()
115+
assert (tdir / "config.json").is_file()
116+
assert (tdir / "diffusion_pytorch_model.safetensors").is_file() or (
117+
tdir / "diffusion_pytorch_model.bin"
118+
).is_file()
119+
120+
def test_import_hf_to_megatron(self, flux_megatron_ckpt_dir: Path) -> None:
121+
"""Asserts the shared class import produced a non-empty Megatron checkpoint."""
122+
assert flux_megatron_ckpt_dir.is_dir()
123+
assert any(flux_megatron_ckpt_dir.iterdir())
124+
125+
def test_export_megatron_to_hf_roundtrip(
126+
self, flux_toy_hf_root: Path, flux_megatron_ckpt_dir: Path, tmp_path: Path
127+
) -> None:
128+
hf_export = tmp_path / "hf_export"
129+
hf_export.mkdir(parents=True, exist_ok=True)
130+
131+
export_cmd = [
132+
"python",
133+
"-m",
134+
"coverage",
135+
"run",
136+
"--data-file=/opt/Megatron-Bridge/.coverage",
137+
"--source=/opt/Megatron-Bridge/",
138+
"--parallel-mode",
139+
CONVERT_SCRIPT,
140+
"export",
141+
"--hf-model",
142+
str(flux_toy_hf_root),
143+
"--megatron-path",
144+
str(flux_megatron_ckpt_dir),
145+
"--hf-path",
146+
str(hf_export),
147+
"--no-progress",
148+
]
149+
er = subprocess.run(export_cmd, capture_output=True, text=True, cwd=REPO_ROOT)
150+
if er.returncode != 0:
151+
print("EXPORT STDOUT:", er.stdout)
152+
print("EXPORT STDERR:", er.stderr)
153+
pytest.fail(f"FLUX export failed with exit code {er.returncode}")
154+
155+
assert "Successfully exported model to:" in er.stdout
156+
transformer_out = hf_export / "transformer"
157+
assert transformer_out.is_dir(), f"Missing {transformer_out}"
158+
assert (transformer_out / "config.json").is_file()
159+
st = transformer_out / "diffusion_pytorch_model.safetensors"
160+
pt = transformer_out / "diffusion_pytorch_model.bin"
161+
assert st.is_file() or pt.is_file(), f"No weights under {transformer_out}"

‎tests/unit_tests/diffusion/model/flux/conversion/test_flux_bridge.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def _make_cfg(
3131
num_single_layers=38,
3232
num_attention_heads=24,
3333
attention_head_dim=128,
34+
joint_attention_dim=4096,
3435
pooled_projection_dim=768,
3536
guidance_embeds=False,
3637
axes_dims_rope=(16, 56, 56),
@@ -42,6 +43,7 @@ def _make_cfg(
4243
cfg.num_single_layers = num_single_layers
4344
cfg.num_attention_heads = num_attention_heads
4445
cfg.attention_head_dim = attention_head_dim
46+
cfg.joint_attention_dim = joint_attention_dim
4547
cfg.pooled_projection_dim = pooled_projection_dim
4648
cfg.guidance_embeds = guidance_embeds
4749
cfg.axes_dims_rope = axes_dims_rope
@@ -74,7 +76,11 @@ def __init__(self, cfg):
7476
assert provider.patch_size == cfg.patch_size
7577
assert provider.vec_in_dim == cfg.pooled_projection_dim
7678
assert provider.guidance_embed == cfg.guidance_embeds
77-
assert provider.axes_dims_rope == cfg.axes_dims_rope
79+
assert provider.axes_dims_rope == list(cfg.axes_dims_rope)
80+
81+
assert provider.hidden_size == cfg.num_attention_heads * cfg.attention_head_dim
82+
assert provider.context_dim == cfg.joint_attention_dim
83+
assert provider.ffn_hidden_size == int(getattr(cfg, "mlp_ratio", 4) * provider.hidden_size)
7884

7985
# bf16 and params_dtype set by bridge
8086
assert provider.bf16 is False

0 commit comments

Comments
 (0)