Skip to content
Open

add moh #4881

Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/experiments/deepseek_v3_pretrain/run_pretrain.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from paddleformers.trainer import (
FP8QuantWeightCallback,
IndexerBiasAdjustCallback,
MoECorrectionBiasAdjustCallback,
MoeExpertsGradScaleCallback,
PdArgumentParser,
Expand Down Expand Up @@ -627,6 +628,15 @@ def main():
moe_router_bias_update_rate = getattr(config, "moe_router_bias_update_rate", 0.001)
callbacks += [MoECorrectionBiasAdjustCallback(moe_router_bias_update_rate)]

if getattr(config, "use_moh", False):
indexer_bias_update_rate = getattr(training_args, "indexer_bias_update_rate", 0.001)
callbacks += [
IndexerBiasAdjustCallback(
lr=indexer_bias_update_rate,
use_mp=getattr(training_args, "sequence_parallel", False),
)
]

def resume_from_custom_func(model):
if training_args.resume_from_huggingface_ckpt:
load_huggingface_ckpt(model, training_args.resume_from_huggingface_ckpt)
Expand Down
10 changes: 10 additions & 0 deletions examples/experiments/paddlefleet/run_pretrain.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
)
from paddleformers.trainer import (
FP8QuantWeightCallback,
IndexerBiasAdjustCallback,
MoECorrectionBiasAdjustCallback,
MoeExpertsGradScaleCallback,
PdArgumentParser,
Expand Down Expand Up @@ -644,6 +645,15 @@ def main():
moe_router_bias_update_rate = getattr(config, "moe_router_bias_update_rate", 0.001)
callbacks += [MoECorrectionBiasAdjustCallback(moe_router_bias_update_rate)]

if getattr(config, "use_moh", False):
indexer_bias_update_rate = getattr(training_args, "indexer_bias_update_rate", 0.001)
callbacks += [
IndexerBiasAdjustCallback(
lr=indexer_bias_update_rate,
use_mp=getattr(training_args, "sequence_parallel", False),
)
]

def resume_from_custom_func(model):
if training_args.resume_from_huggingface_ckpt:
raise NotImplementedError("Resume from HuggingFace ckpt is not supported yet")
Expand Down
13 changes: 13 additions & 0 deletions paddleformers/cli/train/deepseek_v3_pretrain/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
)
from paddleformers.trainer import (
FP8QuantWeightCallback,
IndexerBiasAdjustCallback,
MoECorrectionBiasAdjustCallback,
MoeExpertsGradScaleCallback,
StepFlexToken,
Expand Down Expand Up @@ -557,6 +558,18 @@ def run_dsv3_pretrain(model_args, data_args, generating_args, training_args):
moe_router_bias_update_rate = getattr(config, "moe_router_bias_update_rate", 0.001)
callbacks += [MoECorrectionBiasAdjustCallback(moe_router_bias_update_rate)]

# MoH: indexer_moh_bias load-balance update, active only when the model
# config enables MoH routing (dsv4-hybrid stack). The callback is a no-op
# if no CSAIndexer in the model carries the MoH buffers.
if getattr(config, "use_moh", False):
indexer_bias_update_rate = getattr(training_args, "indexer_bias_update_rate", 0.001)
callbacks += [
IndexerBiasAdjustCallback(
lr=indexer_bias_update_rate,
use_mp=getattr(training_args, "sequence_parallel", False),
)
]

def resume_from_custom_func(model):
if training_args.resume_from_huggingface_ckpt:
load_huggingface_ckpt(model, training_args.resume_from_huggingface_ckpt)
Expand Down
12 changes: 12 additions & 0 deletions paddleformers/cli/train/sft/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from paddleformers.peft import LoRAConfig, LoRAModel
from paddleformers.trainer import (
FP8QuantWeightCallback,
IndexerBiasAdjustCallback,
IntervalStrategy,
MoECorrectionBiasAdjustCallback,
MoeExpertsGradScaleCallback,
Expand Down Expand Up @@ -717,6 +718,17 @@ def fetch_and_serialize(generator, dtype):
elif getattr(model_config.get_text_config(), "topk_method", None) == "quantile_balancing":
callbacks += [MoEQuantileBalancingCallback()]

# MoH: indexer_moh_bias load-balance update. The callback is a no-op when
# no CSAIndexer in the model carries the MoH buffers, so it is safe to add
# whenever ``use_moh`` is on in the config.
if getattr(model_config.get_text_config(), "use_moh", False):
callbacks += [
IndexerBiasAdjustCallback(
lr=getattr(training_args, "indexer_bias_update_rate", 0.001),
use_mp=training_args.sequence_parallel,
)
]

if training_args.use_expert_parallel:
callbacks += [MoeExpertsGradScaleCallback(training_args)]

Expand Down
1 change: 1 addition & 0 deletions paddleformers/trainer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"StepFlexToken",
"FP8QuantWeightCallback",
"MoECorrectionBiasAdjustCallback",
"IndexerBiasAdjustCallback",
"MoEQuantileBalancingCallback",
"MoeExpertsGradScaleCallback",
"MoEGateSpGradSyncCallBack",
Expand Down
65 changes: 65 additions & 0 deletions paddleformers/trainer/trainer_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class StandardMoERouter:
"StepFlexToken",
"FP8QuantWeightCallback",
"MoECorrectionBiasAdjustCallback",
"IndexerBiasAdjustCallback",
"MoEQuantileBalancingCallback",
"MoeExpertsGradScaleCallback",
"MoEGateSpGradSyncCallBack",
Expand Down Expand Up @@ -864,6 +865,70 @@ def update_bias(layer):
model.apply(update_bias)


class IndexerBiasAdjustCallback(TrainerCallback):
"""Per-step sign-based bias update for CSAIndexer MoH load balancing.

Mirrors ``MoECorrectionBiasAdjustCallback`` but targets
``CSAIndexer.indexer_moh_bias`` / ``local_tokens_per_indexer_moh``.
"""

def __init__(self, lr=0.001, use_mp=False):
super().__init__()
self.update_lr = lr
self.use_mp = use_mp

def on_optimizer_end(self, args, state, control, **kwargs):
if getattr(args, "freeze_training", False):
logger.warning("freeze_training is enabled! indexer_moh_bias will NOT be updated.")
return

model = kwargs["model"]
lr_ratio_fn = get_lr_ratio_fn(kwargs.get("optimizer"))

modules_with_bias = [
m
for m in model.sublayers()
if hasattr(m, "indexer_moh_bias") and hasattr(m, "local_tokens_per_indexer_moh")
]
if not modules_with_bias:
return

usages_tensor = paddle.stack(
[m.local_tokens_per_indexer_moh for m in modules_with_bias], axis=0
) # [num_indexers, n_heads]

if hasattr(fleet, "_hcg"):
hcg = fleet.get_hybrid_communicate_group()
mp_group = hcg.get_model_parallel_group()
dp_group = hcg.get_data_parallel_group()
sd_group = hcg.get_sharding_parallel_group()
if self.use_mp and mp_group.nranks > 1:
dist.all_reduce(usages_tensor, group=mp_group)
if dp_group.nranks > 1:
dist.all_reduce(usages_tensor, group=dp_group)
if sd_group.nranks > 1:
dist.all_reduce(usages_tensor, group=sd_group)
else:
dist.all_reduce(usages_tensor)

usages_mean = usages_tensor.mean(-1, keepdim=True) # [num_indexers, 1]
update = paddle.sign(usages_mean - usages_tensor) * self.update_lr # [num_indexers, n_heads]
update = update.astype(paddle.float32)

with paddle.no_grad():
for i, m in enumerate(modules_with_bias):
# Skip if the indexer weights are frozen or lr ratio is 0.
# Use linear_wq_b.weight as the representative trainable param.
ref_param = getattr(getattr(m, "linear_weights_proj", None), "weight", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 新增的 test_stop_gradient_skips_update_but_still_zeroes_counter 只将 linear_wq_b.weight.stop_gradient 设为 True,但这里实际读取的是 linear_weights_proj.weight。测试里的 _FakeCSAIndexer 没有 linear_weights_proj,所以 ref_param 会是 None,随后仍执行 indexer_moh_bias.add_,该测试在依赖齐全时必然失败,也没有验证注释所述的冻结保护。请统一代表参数与 fake/test,并确保冻结分支测试通过。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@risemeup1111 risemeup1111 Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已核对当前实现与测试均使用 linear_weights_proj.weight 作为冻结判定参数,并新增了冻结非代表参数不应触发冻结分支的覆盖。原 P1 已修复。

if ref_param is not None:
frozen = ref_param.stop_gradient or (lr_ratio_fn is not None and not float(lr_ratio_fn(ref_param)))
if frozen:
m.local_tokens_per_indexer_moh.zero_()
continue
m.indexer_moh_bias.add_(update[i])
m.local_tokens_per_indexer_moh.zero_()


class MoEQuantileBalancingCallback(TrainerCallback):
"""PaddleFormers adapter for PaddleFleet's optimizer-step QB update."""

Expand Down
9 changes: 9 additions & 0 deletions paddleformers/trainer/training_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,15 @@ class TrainingArguments:
and decreased for the experts with more assigned tokens."""
},
)
indexer_bias_update_rate: float = field(
default=0.001,
metadata={
"help": """The MoH indexer bias (indexer_moh_bias) is updated based on the number of tokens
routed to each indexer head in a global batch, where the bias is increased for the heads with
fewer assigned tokens and decreased for the heads with more assigned tokens. Only effective
when the model config sets use_moh=True."""
},
)
freeze_training: bool = field(
default=False,
metadata={
Expand Down
21 changes: 21 additions & 0 deletions paddleformers/transformers/configuration_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,27 @@ class LlmMetaConfig:
0.01,
"Loss coefficient for the DSA indexer; controls the weight of the indexer loss term.",
),
(
"use_moh",
bool,
False,
"Whether to enable Mixture-of-Heads (MoH) routing over the CSA indexer heads. "
"Requires `num_activated_heads` to be set.",
),
(
"num_activated_heads",
Optional[int],
None,
"Number of indexer heads kept per token by MoH routing. Only read when `use_moh=True`; "
"must satisfy 1 <= num_activated_heads <= dsa_index_n_heads.",
),
(
"indexer_bias_update_rate",
float,
0.001,
"Update rate for the MoH indexer_moh_bias (used by IndexerBiasAdjustCallback). Controls the "
"magnitude of the per-step sign-based load-balancing adjustment. Defaults to 0.001.",
),
]

mtp_attributes = [
Expand Down
12 changes: 12 additions & 0 deletions paddleformers/transformers/deepseek_v4/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ def __init__(
dsa_index_topk=512,
dsa_indexer_loss_coeff=0.01,
dsa_indexer_use_sparse_loss=True,
# === MoH (Mixture-of-Heads) indexer routing ===
use_moh=False,
num_activated_heads=None,
# === mHC (Hyper-Connection) ===
enable_hyper_connections=True,
num_residual_streams=4,
Expand Down Expand Up @@ -253,6 +256,13 @@ def __init__(
self.dsa_indexer_loss_coeff = dsa_indexer_loss_coeff
self.dsa_indexer_use_sparse_loss = dsa_indexer_use_sparse_loss

# MoH (Mixture-of-Heads) indexer routing. ``num_activated_heads`` is the
# number of indexer heads kept per token; it is only read when
# ``use_moh`` is set, and PaddleFleet's TransformerConfig rejects the
# pair if it is missing or exceeds ``dsa_index_n_heads``.
self.use_moh = use_moh
self.num_activated_heads = num_activated_heads

# mHC (Hyper-Connection)
self.enable_hyper_connections = enable_hyper_connections
self.num_residual_streams = num_residual_streams
Expand Down Expand Up @@ -318,6 +328,8 @@ def __init__(
tensor_model_parallel_size=tensor_model_parallel_size,
pipeline_model_parallel_size=pipeline_model_parallel_size,
context_parallel_size=context_parallel_size,
use_moh=use_moh,
num_activated_heads=num_activated_heads,
**kwargs,
)

Expand Down
13 changes: 13 additions & 0 deletions paddleformers/transformers/deepseek_v4/modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ def _gen_aoa_config(cls, config: DeepseekV4Config):
n_shared_experts = getattr(config, "n_shared_experts", 1)
moe_n_hash_layers = getattr(config, "moe_n_hash_layers", 3)
dense_mode = getattr(config, "csa_dense_mode", False)
use_moh = getattr(config, "use_moh", False)
csa_compress_ratios = config.csa_compress_ratios
num_head_empty_layers = (
config.num_empty_layers_add_in_head
Expand Down Expand Up @@ -518,6 +519,12 @@ def _gen_aoa_config(cls, config: DeepseekV4Config):
f"{idx_src}.weights_proj.weight^T -> {idx_tgt}.linear_weights_proj.weight",
f"{idx_src}.wq_b.weight^T -> {idx_tgt}.linear_wq_b.weight",
]
# V4_INDEXER_MOH: indexer_moh_bias (persistable buffer) is not in the pretrained checkpoint;
# randomly initialize them via add primitive.
if use_moh:
stmts += [
f"_ -> {idx_tgt}.indexer_moh_bias",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 indexer_moh_bias 是可持久化且会在每个 optimizer step 更新的 buffer,但这里仅在 HF -> Fleet 的正向 AOA 中加入 _ -> ...indexer_moh_bias_gen_inv_aoa_config 的 decoder 和 MTP indexer 区块都没有把该 buffer 导出到 HF,因此 save_pretrained 不会保存训练后的 bias;下次从 HF 加载时又会执行这个 add primitive,把 bias 重新初始化为 0,丢失负载均衡状态。请在两个逆向 indexer 映射中补上 Fleet -> HF 的对应语句,并增加保存/加载 round-trip 回归测试。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在当前头部验证:decoder 与 MTP 两处 _gen_inv_aoa_config 都补上了 indexer_moh_bias 的 Fleet -> HF 映射,并新增了正反向覆盖一致性的回归测试。原问题已修复。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

更正:当前头部只补了 Fleet -> HF 导出,HF -> Fleet 仍是 _ -> ...indexer_moh_bias,没有读取保存后的 layers.*.attn.indexer.indexer_moh_bias。因此从刚 save_pretrained 的 HF checkpoint 加载时仍会丢失已训练 bias;新增测试只检查两侧语句存在,并未覆盖真实 save/load round-trip。原 P1 仍未解决,请在正向 AOA 增加已保存 bias 的映射,并仅对没有该字段的旧 checkpoint 做零初始化。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已核对当前头部:decoder 与 MTP 的正向 AOA 会根据 checkpoint key 在已保存 bias 存在时使用命名映射,缺失时才零初始化;保存器始终生成索引,加载端也会读取该 key 集。原 P1 已修复。

]

# --- MoE Gate ---
stmts += [f"{src}.ffn.gate.weight -> {tgt}.mlp.gate.weight, dtype='float32'"]
Expand Down Expand Up @@ -651,6 +658,12 @@ def _gen_aoa_config(cls, config: DeepseekV4Config):
f"{idx_src}.weights_proj.weight^T -> {idx_tgt}.linear_weights_proj.weight",
f"{idx_src}.wq_b.weight^T -> {idx_tgt}.linear_wq_b.weight",
]
# V4_INDEXER_MOH: indexer_moh_bias (persistable buffer) is not in the pretrained checkpoint;
# randomly initialize them via add primitive.
if use_moh:
stmts += [
f"_ -> {idx_tgt}.indexer_moh_bias",
]

# --- MoE Gate (MTP layers are always non-hash, so always have bias) ---
stmts += [
Expand Down
Loading