multimodalart's picture
multimodalart HF Staff
prep stage
1811aab verified
Raw History Blame Contribute Delete
7.68 kB
"""Loading the frozen H3 transformer with a LoRA (trainable or merged), and sharding its blocks."""
from __future__ import annotations
from pathlib import Path
import torch
from safetensors.torch import load_file
from .. import paths
from . import attention
EXPECTED_LORA_MODULES = 200 # the CWM LoRA: qkv / out / fc1 / fc2 of the 50 blocks
def load_base():
"""The released Ref2VA H3 transformer on CPU, bf16, frozen."""
from musubi_tuner.minimax_h3.model import load_h3_transformer
tr = load_h3_transformer(paths.DIT, device="cpu", dtype=torch.bfloat16, attn_mode="flash3", split_attn=False,
disable_mmap=False)
tr.requires_grad_(False)
return tr
def attach_trainable_lora(tr, lora: Path):
"""Wrap the blocks' linears with a LoRA initialised from `lora`. Returns the (fp32-ready) network."""
from musubi_tuner.networks import lora_minimax_h3
sd = load_file(str(lora))
net = lora_minimax_h3.create_arch_network_from_weights(1.0, sd, unet=tr, for_inference=False)
net.apply_to(None, tr, apply_text_encoder=False, apply_unet=True)
info = net.load_state_dict(sd, strict=False)
assert not info.unexpected_keys, info.unexpected_keys[:5]
assert not info.missing_keys and len(net.unet_loras) == EXPECTED_LORA_MODULES, (len(info.missing_keys), len(net.unet_loras))
return net
def _skip_when_off(m):
full = m.forward
def forward(x):
return m.org_forward(x) if m.multiplier == 0 else full(x)
return forward
class LoRAStack:
"""Several LoRAs on one frozen base (distillation: student, critic, teacher). They are stacked on
the same linears; exactly one is active at a time (the others get multiplier 0). Activation
checkpointing recomputes blocks in backward, so the network that built a graph must be active
again when that graph is back-propagated."""
def __init__(self, tr, specs: dict):
"""specs: name -> (initial LoRA file, trainable)."""
self.nets, self.trainable = {}, {}
for name, (path, trainable) in specs.items():
net = attach_trainable_lora(tr, path)
net.requires_grad_(trainable)
self.nets[name], self.trainable[name] = net, trainable
for m in net.unet_loras: # musubi's LoRA computes its branch even at
m.forward = _skip_when_off(m) # multiplier 0: every forward paid for all three
self.active = None
def to(self, device):
for net in self.nets.values():
net.to(device, torch.float32)
return self
def use(self, name: str):
"""Activate one LoRA; only it (if trainable) requires gradients, so a backward pass produces
(and all-reduces) gradients for that network alone."""
for n, net in self.nets.items():
for m in net.unet_loras:
m.multiplier = 1.0 if n == name else 0.0
net.requires_grad_(n == name and self.trainable[n])
self.active = name
def params(self, name: str):
return list(self.nets[name].parameters())
def load_for_inference(lora: Path, device, fp8: bool = False):
"""Base + merged LoRA on `device`, eval mode, causal attention installed; fp8: the blocks' large linears
in FP8 (rtcwm.model.fp8)."""
from cwm_h3_inference.runtime import strict_merge_lora
tr = load_base()
strict_merge_lora(tr, lora, EXPECTED_LORA_MODULES)
tr = tr.to(device).eval()
if fp8:
from .fp8 import quantize_blocks
quantize_blocks(tr)
attention.install(tr)
return tr
def shard_blocks(tr, group, device):
"""FSDP-shard each block over `group` (the GPUs of one node); everything else is small and replicated.
The LoRA weights live in the network module, outside FSDP, and stay replicated."""
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import ShardingStrategy
for name, child in tr.named_children():
if name != "blocks":
child.to(device)
from torch.distributed.fsdp._runtime_utils import _reshard, _unshard
# FSDP (v1) leaves a block's full parameters gathered after a forward run without autograd and after
# a forward inside our activation checkpoint -- every block then stays gathered (+55-60 GB per GPU
# during a KV-cache rollout, a no-grad teacher pass or a student gradient step). And as the base
# weights are frozen, FSDP registers no post-backward hook either, so a block gathered for its
# backward stays gathered as well. We therefore manage the block's gather/reshard ourselves:
def in_backward():
return torch._C._current_graph_task_id() != -1
def gather_for_recompute(module, _inputs):
# FSDP gathers a block for backward once per backward pass; a block used by several gradient
# forwards (several student chunks) is recomputed again after we resharded it
if in_backward() and module._handle.needs_unshard():
_unshard(module, module._handle, module._unshard_stream, module._pre_unshard_stream)
torch.cuda.current_stream().wait_stream(module._unshard_stream)
def reshard_after_forward(module, inputs, _output):
if in_backward(): # checkpoint recomputation: the backward of the block follows
return
_reshard(module, module._handle, True)
h = inputs[0] if inputs and isinstance(inputs[0], torch.Tensor) else None
if h is not None and h.requires_grad: # the block's backward is done once its input has a gradient
h.register_hook(lambda g: (_reshard(module, module._handle, True), g)[1])
# Every block is its own FSDP root, so FSDP's forward_prefetch (which follows one root's execution
# order) never gathers the next block. Outside backward we do it by hand: once block i holds its
# parameters (a pre-hook on the wrapped module, i.e. inside FSDP's forward, after its unshard), block
# i+1's all-gather is issued on its own unshard stream and overlaps block i's compute; FSDP's
# pre-forward of block i+1 then finds it prefetched and only waits for that stream. (The same in
# backward -- block i-1 during block i's recomputation -- measured no faster: the backward is compute
# bound.)
from torch.distributed.fsdp._flat_param import HandleTrainingState
def prefetch_next(i):
def hook(_module, _inputs):
if in_backward() or i + 1 >= len(tr.blocks):
return
nxt = tr.blocks[i + 1]
h = nxt._handle
if (h is None or getattr(nxt, "_unshard_stream", None) is None # not run yet: FSDP initialises
or not hasattr(h.flat_param, "_full_param_padded") # a block lazily, in its first
or h._prefetched or not h.needs_unshard()): # forward
return
prev, h._training_state = h._training_state, HandleTrainingState.FORWARD
_unshard(nxt, h, nxt._unshard_stream, nxt._pre_unshard_stream)
h._training_state, h._prefetched = prev, True
return hook
for i in range(len(tr.blocks)):
tr.blocks[i] = FSDP(tr.blocks[i], process_group=group, device_id=device, use_orig_params=True,
sharding_strategy=ShardingStrategy.FULL_SHARD, limit_all_gathers=True)
tr.blocks[i].register_forward_pre_hook(gather_for_recompute)
tr.blocks[i].register_forward_hook(reshard_after_forward)
tr.blocks[i].module.register_forward_pre_hook(prefetch_next(i))
attention.install(tr)
return tr