Make ResBlock's conditioning-injection mechanism selectable (gitea #34)
CI / Format (ruff format) (push) Successful in 29s
CI / Lint (ruff check) (push) Successful in 33s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 32s
CI / Tests (push) Successful in 1m58s
CI / Format (ruff format) (pull_request) Successful in 28s
CI / Lint (ruff check) (pull_request) Successful in 31s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 27s
CI / Tests (pull_request) Successful in 2m5s

ResBlock injected conditioning exactly one way — h = linear1(h) +
cond_proj(cond), a conditional bias, the weakest standard option for a
model whose entire job is to be conditional. Adds BLOCK_REGISTRY
(giant/model/layers.py), mirroring the TRUNK_REGISTRY/ROUTER_REGISTRY
registry+factory idiom (gitea #33), with two new drop-in alternatives:
FilmResBlock (per-channel scale+shift modulating the norm output,
zero-init so conditioning has no effect at construction) and
AdaLNResBlock (DiT-style AdaLN-Zero — the norm's own affine is replaced
by a conditioning-derived scale/shift, plus a zero-init gate on the
residual branch, making the block the exact identity function at init).

Selected per stage via a new stage{1,2}_model.trunk.block_conditioning
config leaf ("add" | "film" | "adaln", default "add"), threaded through
build_trunk/build_expert_body/RoutedTrunk and the three stage model
constructors. Default stays "add" and ResBlock's body is unchanged, so
existing configs/checkpoints are bit-identical to before this change.

Decided during planning: the new field lives on the existing TrunkConfig
rather than a new top-level block/blocks config section; the WGAN
CriticModel (which builds its own ResBlock stack outside TRUNK_REGISTRY)
and the issue's mentioned blocks.norm/blocks.activation axes are both
left out of scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 09:41:50 +02:00
parent dc4cad7d11
commit 0f95e0eaae
8 changed files with 280 additions and 14 deletions
+29
View File
@@ -85,6 +85,15 @@ def test_trunk_config_defaults_to_resmlp_for_both_stages():
assert gconfig.DEFAULT_CONFIG["stage2_model"]["trunk"]["type"] == "resmlp"
def test_trunk_config_defaults_block_conditioning_to_add_for_both_stages():
"""gitea #34: a pre-existing config with no `block_conditioning` key
must reproduce today's additive-bias behaviour exactly."""
assert gconfig.Stage1ModelConfig().trunk.block_conditioning == "add"
assert gconfig.Stage2ModelConfig().trunk.block_conditioning == "add"
assert gconfig.DEFAULT_CONFIG["stage1_model"]["trunk"]["block_conditioning"] == "add"
assert gconfig.DEFAULT_CONFIG["stage2_model"]["trunk"]["block_conditioning"] == "add"
def test_particle_type_config_n_classes_defaults_to_zero_and_round_trips():
"""gitea #29: n_classes=0 means "inherit conditioning.particle.emb_dim"
— the default must stay 0 so an existing config.toml with no
@@ -936,6 +945,26 @@ def test_validate_config_keys_rejects_unknown_trunk_key():
assert "type" in str(e)
def test_validate_config_keys_allows_block_conditioning():
cfg = _cfg_with(
**{
"stage1_model.trunk.block_conditioning": "film",
"stage2_model.trunk.block_conditioning": "adaln",
}
)
gconfig.validate_config_keys(cfg) # must not raise
def test_validate_config_keys_rejects_unknown_block_conditioning_key():
cfg = _cfg_with(**{"stage1_model.trunk.block_conditioning_o": "film"}) # typo
try:
gconfig.validate_config_keys(cfg)
assert False, "expected ValueError"
except ValueError as e:
assert "stage1_model.trunk.block_conditioning_o" in str(e)
assert "block_conditioning" in str(e)
def test_merge_cli_overrides_rejects_typo_in_toml_file(tmp_path):
path = tmp_path / "config.toml"
path.write_text("[meta]\nconfig_version = 3\n\n[stage1_model]\nn_res_block = 12\n")
+89
View File
@@ -5,16 +5,21 @@ import torch
from giant.constants import COND_DIM, K_MAX, SEC_DIM, X_DIM
from giant.model.network import (
BLOCK_REGISTRY,
TRUNK_REGISTRY,
AdaLNResBlock,
ComposedRouter,
EnergyRouter,
ExpertTrunk,
FilmResBlock,
PdgRouter,
ProcessRouter,
ROUTER_REGISTRY,
ResBlock,
RoutedTrunk,
Stage1Model,
Stage2OneShot,
build_block,
build_composed_router,
build_expert_body,
build_models,
@@ -162,6 +167,52 @@ def test_build_expert_body_unknown_type_raises():
raise AssertionError("expected ValueError for unknown trunk type")
# ── BLOCK_REGISTRY / build_block (gitea #34) ────────────────────────────────
def test_block_registry_has_add_film_adaln():
assert BLOCK_REGISTRY["add"] is ResBlock
assert BLOCK_REGISTRY["film"] is FilmResBlock
assert BLOCK_REGISTRY["adaln"] is AdaLNResBlock
def test_build_block_unknown_type_raises():
try:
build_block("nonexistent", dim=8, cond_dim=4)
except ValueError:
return
raise AssertionError("expected ValueError for unknown block conditioning type")
@pytest.mark.parametrize("block_type", ["add", "film", "adaln"])
def test_block_forward_shape(block_type):
block = build_block(block_type, dim=8, cond_dim=4)
x = torch.randn(5, 8)
cond = torch.randn(5, 4)
out = block(x, cond)
assert out.shape == (5, 8)
def test_film_res_block_output_invariant_to_cond_at_init():
"""Zero-initialized film_proj means gamma=beta=0 at construction, so the
output must not depend on which cond is passed in."""
block = FilmResBlock(dim=8, cond_dim=4)
x = torch.randn(5, 8)
cond_a = torch.randn(5, 4)
cond_b = torch.randn(5, 4)
torch.testing.assert_close(block(x, cond_a), block(x, cond_b))
def test_adaln_res_block_is_identity_at_init():
"""Zero-initialized adaln_proj means scale=shift=gate=0 at construction,
so the block must be the exact identity function (the 'Zero' in
AdaLN-Zero)."""
block = AdaLNResBlock(dim=8, cond_dim=4)
x = torch.randn(5, 8)
cond = torch.randn(5, 4)
torch.testing.assert_close(block(x, cond), x)
# ── EnergyRouter learn_width / learn_temperature ────────────────────────────
@@ -1077,6 +1128,44 @@ def test_build_models_explicit_resmlp_trunk_type_matches_default():
assert default_params == explicit_params
def test_build_models_explicit_add_block_conditioning_matches_default():
"""stage1_model.trunk.block_conditioning = 'add' is the default's
spelled-out equivalent, not a behaviour change — gitea #34."""
default_cfg = _nested_cfg(pdg_vocab=4, mat_vocab=2)
explicit_cfg = _nested_cfg(pdg_vocab=4, mat_vocab=2, trunk={"type": "resmlp", "block_conditioning": "add"})
default_stage1 = build_models(default_cfg)["stage1"]
explicit_stage1 = build_models(explicit_cfg)["stage1"]
assert default_stage1 is not None and explicit_stage1 is not None
assert type(default_stage1.trunk.blocks[0]) is type(explicit_stage1.trunk.blocks[0]) is ResBlock
default_params = sum(p.numel() for p in default_stage1.parameters())
explicit_params = sum(p.numel() for p in explicit_stage1.parameters())
assert default_params == explicit_params
@pytest.mark.parametrize("block_type,cls", [("film", FilmResBlock), ("adaln", AdaLNResBlock)])
def test_build_models_selects_block_conditioning(block_type, cls):
cfg = _nested_cfg(pdg_vocab=4, mat_vocab=2, trunk={"type": "resmlp", "block_conditioning": block_type})
stage1 = build_models(cfg)["stage1"]
assert stage1 is not None
assert isinstance(stage1.trunk, ExpertTrunk)
assert all(isinstance(b, cls) for b in stage1.trunk.blocks)
def test_build_models_routed_trunk_uses_block_conditioning_for_every_expert():
cfg = _nested_cfg(
pdg_vocab=4,
mat_vocab=2,
trunk={"type": "resmlp", "block_conditioning": "film"},
stage1_router={"enabled": True, "type": "energy", "n_experts": 3},
)
stage1 = build_models(cfg)["stage1"]
assert stage1 is not None
assert isinstance(stage1.trunk, RoutedTrunk)
assert len(stage1.trunk.experts) == 3
for expert in stage1.trunk.experts:
assert all(isinstance(b, FilmResBlock) for b in expert.blocks)
def test_build_models_routed_pair_is_drop_in_for_sample_flow():
"""Exercise the exact calling convention giant/sample.py uses."""
from giant.sample import sample_flow, sample_secondaries