From f93835c350e827ea69691ca078924ddefa65138f Mon Sep 17 00:00:00 2001 From: shaojiemike Date: Thu, 25 Jun 2026 11:18:16 +0800 Subject: [PATCH 01/11] Migrate dFactory core to latest VeOmni with NPU support --- VeOmni | 2 +- configs/sft/llada2_flash_bd_sft.yaml | 83 +-- configs/sft/llada2_flash_bd_sft_npu.yaml | 76 +++ configs/sft/llada2_mini_bd_sft.yaml | 83 +-- configs/sft/llada2_mini_bd_sft_npu.yaml | 76 +++ models/llada2_moe/__init__.py | 29 +- models/llada2_moe/modeling_llada2_moe.py | 109 +++- tasks/dataset/__init__.py | 2 +- tasks/train_llada2_bd.py | 568 +------------------ tasks/train_llada2_bd_with_dparallel.py | 620 +-------------------- tasks/train_llada2_common.py | 663 +++++++++++++++++++++++ train.sh | 11 +- 12 files changed, 1052 insertions(+), 1270 deletions(-) create mode 100644 configs/sft/llada2_flash_bd_sft_npu.yaml create mode 100644 configs/sft/llada2_mini_bd_sft_npu.yaml create mode 100644 tasks/train_llada2_common.py diff --git a/VeOmni b/VeOmni index 600fe6d..8ca09d7 160000 --- a/VeOmni +++ b/VeOmni @@ -1 +1 @@ -Subproject commit 600fe6d7442392fd3ddefad4b6c8d3c0002fed1c +Subproject commit 8ca09d7c87f06ee7c0f69b0ca0c9e9a6b37f2280 diff --git a/configs/sft/llada2_flash_bd_sft.yaml b/configs/sft/llada2_flash_bd_sft.yaml index 603d4bd..11232b2 100644 --- a/configs/sft/llada2_flash_bd_sft.yaml +++ b/configs/sft/llada2_flash_bd_sft.yaml @@ -2,58 +2,75 @@ model: config_path: ./configs/model_configs/llada2_flash model_path: ./LLaDA2.0-flash-preview-moe-merge tokenizer_path: ./LLaDA2.0-flash-preview-moe-merge - attn_implementation: sdpa - moe_implementation: fused + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton + cross_entropy_loss_implementation: eager + rms_norm_implementation: eager + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: eager + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager data: train_path: ./gsm8k_datasets/gsm8k_train.jsonl data_type: conversation datasets_type: mapping - dataloader_type: native max_seq_len: 2048 text_keys: messages noise_range_low: 0.3 noise_range_high: 0.8 - num_workers: 16 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true train: - output_dir: ./llada2_flash_bd_sft_outputs - data_parallel_mode: fsdp2 - tensor_parallel_size: 1 - ulysses_parallel_size: 1 - expert_parallel_size: 1 + dyn_bsz: false global_batch_size: 16 micro_batch_size: 1 num_train_epochs: 1 - rmpad: false - rmpad_with_pos_ids: false bsz_warmup_ratio: 0.007 - dyn_bsz_margin: 0 - dyn_bsz_buffer_size: 200 - optimizer: adamw - beta1: 0.9 - beta2: 0.999 - lr: 1.0e-5 - lr_warmup_ratio: 0.03 - lr_decay_style: cosine - lr_decay_ratio: 1.0 - weight_decay: 0.1 - max_grad_norm: 1.0 - enable_mixed_precision: true - enable_gradient_checkpointing: true - enable_full_shard: true - enable_fsdp_offload: true - enable_activation_offload: false init_device: meta broadcast_model_weights_from_rank0: true enable_full_determinism: false empty_cache_steps: 500 - ckpt_manager: dcp - load_checkpoint_path: "" - save_epochs: 1 - save_hf_weights: true + beta1: 0.9 + beta2: 0.999 block_diffusion_mode: true block_size: 32 same_token_labels: true - use_wandb: false # or you can set `wandb_project` and `wandb_name` to trace your training - log_steps: 1 + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_flash_bd_sft_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/sft/llada2_flash_bd_sft_npu.yaml b/configs/sft/llada2_flash_bd_sft_npu.yaml new file mode 100644 index 0000000..615182e --- /dev/null +++ b/configs/sft/llada2_flash_bd_sft_npu.yaml @@ -0,0 +1,76 @@ +model: + config_path: ./configs/model_configs/llada2_flash + model_path: ./LLaDA2.0-flash-preview-moe-merge + tokenizer_path: ./LLaDA2.0-flash-preview-moe-merge + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_npu + cross_entropy_loss_implementation: npu + rms_norm_implementation: npu + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: npu + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager + +data: + train_path: ./gsm8k_datasets/gsm8k_train.jsonl + data_type: conversation + datasets_type: mapping + max_seq_len: 2048 + text_keys: messages + noise_range_low: 0.3 + noise_range_high: 0.8 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true + +train: + dyn_bsz: false + global_batch_size: 16 + micro_batch_size: 1 + num_train_epochs: 1 + bsz_warmup_ratio: 0.007 + init_device: meta + broadcast_model_weights_from_rank0: true + enable_full_determinism: false + empty_cache_steps: 500 + beta1: 0.9 + beta2: 0.999 + block_diffusion_mode: true + block_size: 32 + same_token_labels: true + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_flash_bd_sft_npu_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/sft/llada2_mini_bd_sft.yaml b/configs/sft/llada2_mini_bd_sft.yaml index 5e188e0..8d126e8 100644 --- a/configs/sft/llada2_mini_bd_sft.yaml +++ b/configs/sft/llada2_mini_bd_sft.yaml @@ -2,58 +2,75 @@ model: config_path: ./configs/model_configs/llada2_mini model_path: ./LLaDA2.0-mini-preview-moe-merge tokenizer_path: ./LLaDA2.0-mini-preview-moe-merge - attn_implementation: sdpa - moe_implementation: fused + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton + cross_entropy_loss_implementation: eager + rms_norm_implementation: eager + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: eager + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager data: train_path: ./gsm8k_datasets/gsm8k_train.jsonl data_type: conversation datasets_type: mapping - dataloader_type: native max_seq_len: 2048 text_keys: messages noise_range_low: 0.3 noise_range_high: 0.8 - num_workers: 16 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true train: - output_dir: ./llada2_mini_bd_sft_outputs - data_parallel_mode: fsdp2 - tensor_parallel_size: 1 - ulysses_parallel_size: 1 - expert_parallel_size: 1 + dyn_bsz: false global_batch_size: 8 micro_batch_size: 1 num_train_epochs: 1 - rmpad: false - rmpad_with_pos_ids: false bsz_warmup_ratio: 0.007 - dyn_bsz_margin: 0 - dyn_bsz_buffer_size: 200 - optimizer: adamw - beta1: 0.9 - beta2: 0.999 - lr: 1.0e-5 - lr_warmup_ratio: 0.03 - lr_decay_style: cosine - lr_decay_ratio: 1.0 - weight_decay: 0.1 - max_grad_norm: 1.0 - enable_mixed_precision: true - enable_gradient_checkpointing: true - enable_full_shard: true - enable_fsdp_offload: true - enable_activation_offload: false init_device: meta broadcast_model_weights_from_rank0: true enable_full_determinism: false empty_cache_steps: 500 - ckpt_manager: dcp - load_checkpoint_path: "" - save_epochs: 1 - save_hf_weights: true + beta1: 0.9 + beta2: 0.999 block_diffusion_mode: true block_size: 32 same_token_labels: true - use_wandb: false # or you can set `wandb_project` and `wandb_name` to trace your training - log_steps: 1 + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_mini_bd_sft_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/sft/llada2_mini_bd_sft_npu.yaml b/configs/sft/llada2_mini_bd_sft_npu.yaml new file mode 100644 index 0000000..35faf5c --- /dev/null +++ b/configs/sft/llada2_mini_bd_sft_npu.yaml @@ -0,0 +1,76 @@ +model: + config_path: ./configs/model_configs/llada2_mini + model_path: ./LLaDA2.0-mini-preview-moe-merge + tokenizer_path: ./LLaDA2.0-mini-preview-moe-merge + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_npu + cross_entropy_loss_implementation: npu + rms_norm_implementation: npu + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: npu + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager + +data: + train_path: ./gsm8k_datasets/gsm8k_train.jsonl + data_type: conversation + datasets_type: mapping + max_seq_len: 2048 + text_keys: messages + noise_range_low: 0.3 + noise_range_high: 0.8 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true + +train: + dyn_bsz: false + global_batch_size: 8 + micro_batch_size: 1 + num_train_epochs: 1 + bsz_warmup_ratio: 0.007 + init_device: meta + broadcast_model_weights_from_rank0: true + enable_full_determinism: false + empty_cache_steps: 500 + beta1: 0.9 + beta2: 0.999 + block_diffusion_mode: true + block_size: 32 + same_token_labels: true + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_mini_bd_sft_npu_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/models/llada2_moe/__init__.py b/models/llada2_moe/__init__.py index 1f22972..a83d5e4 100644 --- a/models/llada2_moe/__init__.py +++ b/models/llada2_moe/__init__.py @@ -1,3 +1,28 @@ -from .modeling_llada2_moe import LLaDA2MoeModelLM +from veomni.models.loader import MODEL_CONFIG_REGISTRY, MODELING_REGISTRY -ModelClass = LLaDA2MoeModelLM \ No newline at end of file +from .configuration_llada2_moe import LLaDA2MoeConfig +from .modeling_llada2_moe import LLaDA2MoeModel, LLaDA2MoeModelLM, LLaDA2MoePreTrainedModel + + +@MODEL_CONFIG_REGISTRY.register("llada2_moe_veomni") +def register_llada2_moe_config(): + return LLaDA2MoeConfig + + +@MODELING_REGISTRY.register("llada2_moe_veomni") +def register_llada2_moe_modeling(architecture: str): + if architecture and ("ForCausalLM" in architecture or "ModelLM" in architecture): + return LLaDA2MoeModelLM + if architecture and "Model" in architecture: + return LLaDA2MoeModel + return LLaDA2MoeModelLM + +ModelClass = LLaDA2MoeModelLM + +__all__ = [ + "LLaDA2MoeConfig", + "LLaDA2MoeModel", + "LLaDA2MoeModelLM", + "LLaDA2MoePreTrainedModel", + "ModelClass", +] diff --git a/models/llada2_moe/modeling_llada2_moe.py b/models/llada2_moe/modeling_llada2_moe.py index 820dd42..c0c0f22 100644 --- a/models/llada2_moe/modeling_llada2_moe.py +++ b/models/llada2_moe/modeling_llada2_moe.py @@ -43,21 +43,35 @@ ) from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update from transformers.modeling_utils import PreTrainedModel, ALL_ATTENTION_FUNCTIONS -from transformers.pytorch_utils import ALL_LAYERNORM_LAYERS, is_torch_greater_or_equal_than_1_13 +from transformers.pytorch_utils import ALL_LAYERNORM_LAYERS +try: + from transformers.pytorch_utils import is_torch_greater_or_equal_than_1_13 +except ImportError: + def is_torch_greater_or_equal_than_1_13(): + return True from transformers.utils import ( add_start_docstrings, add_start_docstrings_to_model_forward, replace_return_docstrings, ) -from transformers.utils.import_utils import is_torch_fx_available +try: + from transformers.utils.import_utils import is_torch_fx_available +except ImportError: + def is_torch_fx_available(): + return True from .configuration_llada2_moe import LLaDA2MoeConfig from transformers.generation.utils import GenerationMixin -from veomni.ops import causallm_loss_function, fused_moe_forward +from veomni.ops import fused_moe_forward from veomni.distributed.parallel_state import get_parallel_state -from veomni.utils.import_utils import is_liger_kernel_available +from veomni.utils.import_utils import is_liger_kernel_available, is_torch_npu_available from veomni.utils import logging -if is_liger_kernel_available(): + +def _liger_kernel_enabled(): + return is_liger_kernel_available() and not is_torch_npu_available() + + +if _liger_kernel_enabled(): from liger_kernel.ops.swiglu import LigerSiLUMulFunction from liger_kernel.transformers.rms_norm import LigerRMSNorm from liger_kernel.transformers.rope import liger_rotary_pos_emb @@ -74,6 +88,40 @@ logger = logging.get_logger(__name__) _CONFIG_FOR_DOC = "LLaDA2MoeConfig" +_LLADA2_MOE_OPS_PATCHED_IMPL = None + + +def _apply_llada2_moe_ops_config(): + global _LLADA2_MOE_OPS_PATCHED_IMPL + + try: + from veomni.ops.config.singleton import get_ops_config + from veomni.ops.kernels.moe import apply_veomni_fused_moe_patch + except Exception: + return + + ops_config = get_ops_config() + if ops_config is None: + return + + moe_impl = getattr(ops_config, "moe_implementation", "eager") + if moe_impl == "eager" or moe_impl == _LLADA2_MOE_OPS_PATCHED_IMPL: + return + + apply_veomni_fused_moe_patch(fused_moe_kernel=moe_impl.removeprefix("fused_")) + _LLADA2_MOE_OPS_PATCHED_IMPL = moe_impl + + +def _get_llada2_moe_implementation(): + try: + from veomni.ops.config.singleton import get_ops_config + except Exception: + return "eager" + + ops_config = get_ops_config() + if ops_config is None: + return "eager" + return getattr(ops_config, "moe_implementation", "eager") def _get_unpad_data(attention_mask): @@ -108,6 +156,24 @@ def forward(self, hidden_states): ALL_LAYERNORM_LAYERS.append(LLaDA2MoeRMSNorm) +def _llada2_default_rope_init(config: LLaDA2MoeConfig, device=None): + head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads) + partial_rotary_factor = getattr(config, "partial_rotary_factor", 1.0) + dim = int(head_dim * partial_rotary_factor) + inv_freq = 1.0 / ( + config.rope_theta ** (torch.arange(0, dim, 2, dtype=torch.int64, device=device).float() / dim) + ) + return inv_freq, 1.0 + + +def _get_rope_init_fn(rope_type: str): + if rope_type in ROPE_INIT_FUNCTIONS: + return ROPE_INIT_FUNCTIONS[rope_type] + if rope_type == "default": + return _llada2_default_rope_init + raise KeyError(rope_type) + + class LLaDA2MoeRotaryEmbedding(nn.Module): def __init__(self, config: LLaDA2MoeConfig, device=None): super().__init__() @@ -120,7 +186,7 @@ def __init__(self, config: LLaDA2MoeConfig, device=None): self.original_max_seq_len = config.max_position_embeddings self.config = config - self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + self.rope_init_fn = _get_rope_init_fn(self.rope_type) inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device) self.register_buffer("inv_freq", inv_freq, persistent=False) @@ -202,7 +268,7 @@ def __init__(self, config: LLaDA2MoeConfig, intermediate_size: int): self.act_fn = ACT2FN[config.hidden_act] def forward(self, x): - if is_liger_kernel_available(): + if _liger_kernel_enabled(): return self.down_proj(LigerSiLUMulFunction.apply(self.gate_proj(x), self.up_proj(x))) else: return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) @@ -275,6 +341,7 @@ def forward(self, hidden_states): class LLaDA2MoeExperts(nn.Module): def __init__(self, config): super().__init__() + _apply_llada2_moe_ops_config() self.num_experts = config.num_experts self.hidden_dim = config.hidden_size self.intermediate_size = config.moe_intermediate_size @@ -306,9 +373,8 @@ def forward(self, hidden_states, expert_idx=None, routing_weights=None, selected ) out = fused_moe_forward( - module=self, num_experts=self.num_experts, - routing_weights=routing_weights, + routing_weights=routing_weights.to(hidden_states.dtype), selected_experts=selected_experts, hidden_states=hidden_states, fc1_1_weight=self.gate_proj, @@ -343,7 +409,7 @@ def __init__(self, config: LLaDA2MoeConfig): self._setup_experts() self.gate = LLaDA2MoeGate(config) - if config.num_shared_experts is not None: + if config.num_shared_experts: self.shared_experts = LLaDA2MoeMLP( config=config, intermediate_size=config.moe_intermediate_size * config.num_shared_experts ) @@ -364,13 +430,24 @@ def _fuse_moe_forward(self, hidden_states): bsz, seq_len, h = hidden_states.shape topk_idx, topk_weight, router_logits = self.gate(hidden_states) hidden_states = hidden_states.view(-1, hidden_states.shape[-1]) - y = self.experts( - hidden_states, routing_weights=topk_weight, selected_experts=topk_idx - ).reshape(bsz, seq_len, h) - if self.config.num_shared_experts is not None: + if _get_llada2_moe_implementation() == "eager": + y = self._stacked_experts_eager_forward(hidden_states, topk_idx, topk_weight).reshape(bsz, seq_len, h) + else: + y = self.experts( + hidden_states, routing_weights=topk_weight, selected_experts=topk_idx + ).reshape(bsz, seq_len, h) + if self.config.num_shared_experts: y = y + self.shared_experts(identity) return y, (router_logits.view(bsz, seq_len, -1), topk_idx.view(bsz, seq_len, -1)) + def _stacked_experts_eager_forward(self, hidden_states, topk_idx, topk_weight): + flat_topk_idx = topk_idx.view(-1) + hidden_states = hidden_states.repeat_interleave(self.num_experts_per_tok, dim=0) + y = torch.empty_like(hidden_states) + for i in range(self.config.num_experts): + y[flat_topk_idx == i] = self.experts(hidden_states[flat_topk_idx == i], expert_idx=i) + return (y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1)).sum(dim=1).to(hidden_states.dtype) + def _forward(self, hidden_states): identity = hidden_states bsz, seq_len, h = hidden_states.shape @@ -386,7 +463,7 @@ def _forward(self, hidden_states): y = y.to(hidden_states.dtype).view(bsz, seq_len, h) else: y = self.moe_infer(hidden_states, topk_idx, topk_weight).view(bsz, seq_len, h) - if self.config.num_shared_experts is not None: + if self.config.num_shared_experts: y = y + self.shared_experts(identity) return y, (router_logits.view(bsz, seq_len, -1), topk_idx.view(bsz, seq_len, -1)) @@ -1566,7 +1643,7 @@ def apply_rotary_pos_emb_llada2_moe(q, k, cos, sin, position_ids, unsqueeze_dim= return q_embed, k_embed -if is_liger_kernel_available(): +if _liger_kernel_enabled(): apply_rotary_pos_emb = apply_rotary_pos_emb_llada2_moe LLaDA2MoeRMSNorm = LigerRMSNorm logger.info_rank0("Apply liger kernel to LLaDA2Moe") diff --git a/tasks/dataset/__init__.py b/tasks/dataset/__init__.py index 8c8e893..a866cc9 100644 --- a/tasks/dataset/__init__.py +++ b/tasks/dataset/__init__.py @@ -3,4 +3,4 @@ __all__ = [ "build_local_dataset" -] \ No newline at end of file +] diff --git a/tasks/train_llada2_bd.py b/tasks/train_llada2_bd.py index ec9ccdb..f00a321 100644 --- a/tasks/train_llada2_bd.py +++ b/tasks/train_llada2_bd.py @@ -1,572 +1,8 @@ -import json -import os -import time -from dataclasses import asdict, dataclass, field -from functools import partial -from typing import Any, Dict, List, Literal, Tuple, Optional - -import torch -import torch.distributed as dist -import wandb -from tqdm import trange - -from veomni.checkpoint import build_checkpointer, ckpt_to_state_dict -from veomni.data import ( - build_dataloader, - build_iterative_dataset, - build_mapping_dataset, -) -from veomni.distributed.offloading import build_activation_offloading_context -from veomni.distributed.parallel_state import get_parallel_state, init_parallel_state -from veomni.distributed.torch_parallelize import build_parallelize_model -from veomni.models import build_foundation_model, build_tokenizer, save_model_assets, save_model_weights -from veomni.optim import build_lr_scheduler, build_optimizer -from veomni.utils import helper -from veomni.utils.arguments import DataArguments, ModelArguments, TrainingArguments, parse_args, save_args -from veomni.utils.device import ( - get_device_type, - get_nccl_backend, - get_torch_device, - synchronize, -) -from veomni.utils.dist_utils import all_reduce -from veomni.models.registry import ModelRegistry -ModelRegistry.register_modeling_path("models.llada2_moe") -from dataset.data_transform import process_mdm_tokenized_example, process_mdm_sft_example -from dataset import build_local_dataset - - -logger = helper.create_logger(__name__) - -@dataclass -class LLaDA2ModelArguments(ModelArguments): - attn_implementation: Optional[Literal["eager", "sdpa", "flex_attention"]] = field( - default="sdpa", - metadata={"help": "Attention implementation to use."}, - ) - - -@dataclass -class LLaDA2DataArguments(DataArguments): - data_type: Literal["conversation", "tokenid"] = field( - default="conversation", - metadata={"help": "Type of the training data."}, - ) - datasets_type: Literal["mapping", "local"] = field( - default="mapping", - metadata={"help": "Type of the datasets."}, - ) - text_keys: str = field( - default="messages", - metadata={"help": "Key to get text from the training data."}, - ) - noise_range_low: float = field( - default=0.3, - metadata={"help": "Noise level for random flip input_ids to mask_ids"} - ) - noise_range_high: float = field( - default=0.8, - metadata={"help": "Noise level for random flip input_ids to mask_ids"} - ) - - def __post_init__(self): - super().__post_init__() - if self.noise_range_low > self.noise_range_high: - raise ValueError( - f"noise_range_low ({self.noise_range_low}) " - f"cannot be greater than noise_range_high ({self.noise_range_high})." - ) - - if not (0.0 <= self.noise_range_low <= 1.0): - raise ValueError( - f"noise_range_low must be between 0.0 and 1.0, but got {self.noise_range_low}." - ) - - if not (0.0 <= self.noise_range_high <= 1.0): - raise ValueError( - f"noise_range_high must be between 0.0 and 1.0, but got {self.noise_range_high}." - ) - - -@dataclass -class LLaDA2TrainingArguments(TrainingArguments): - beta1: float = field( - default=0.9, - metadata={"help": "AdamW optimizer beta1."}, - ) - beta2: float = field( - default=0.999, - metadata={"help": "AdamW optimizer beta2"}, - ) - block_diffusion_mode: bool = field( - default=False, - metadata={"help": "If train MDM in block_diffusion mode. True: use block_diffusion, False: full_attention"} - ) - block_size: int = field( - default=32, - metadata={"help": "The block size for block diffusion block size"} - ) - same_token_labels: bool = field( - default=False, - metadata={"help": "If use same token location labels. True: no shift, False: use next-token prediction shift."} - ) - - -@dataclass -class Arguments: - model: "LLaDA2ModelArguments" = field(default_factory=LLaDA2ModelArguments) - data: "LLaDA2DataArguments" = field(default_factory=LLaDA2DataArguments) - train: "LLaDA2TrainingArguments" = field(default_factory=LLaDA2TrainingArguments) - - -def block_diffusion_mask(b, h, q_idx, kv_idx, block_size=None, n=None): - """ - Constructs the specialized block diffusion attention mask for training - composed of three masks: - - **Block Diagonal Mask (M_BD)**: Self-attention within noised blocks - - **Offset Block Causal Mask (M_OBC)**: Cross-attention for conditional context - - **Block Causal Mask (M_BC)**: Attention to update x0 - - Args: - b, h: Batch and head indices (ignored for mask logic). - q_idx, kv_idx: Query and Key indices. - seq_len: Total sequence length. - block_size: Defines the block structure. - - Returns: - A boolean attention mask. - """ - - # Indicate whether token belongs to xt or x0 - x0_flag_q = (q_idx >= n) - x0_flag_kv = (kv_idx >= n) - - # Compute block indices - block_q = torch.where(x0_flag_q == 1, - (q_idx - n) // block_size, - q_idx // block_size) - block_kv = torch.where(x0_flag_kv == 1, - (kv_idx - n) // block_size, - kv_idx // block_size) - - # **1. Block Diagonal Mask (M_BD) ** - block_diagonal = (block_q == block_kv) & (x0_flag_q == x0_flag_kv) - - # **2. Offset Block-Causal Mask (M_OBC) ** - offset_block_causal = ( - (block_q > block_kv) - & (x0_flag_kv == 1) - & (x0_flag_q == 0) - ) - - # **3. Block-Causal Mask (M_BC) ** - block_causal = (block_q >= block_kv) & (x0_flag_kv == 1) & (x0_flag_q == 1) - - # **4. Combine Masks ** - return block_diagonal | offset_block_causal | block_causal +from train_llada2_common import LLaDA2Arguments, run_llada2_training def main(): - dist.init_process_group(backend=get_nccl_backend()) - args = parse_args(Arguments) - logger.info(f"Process rank: {args.train.global_rank}, world size: {args.train.world_size}") - logger.info_rank0(json.dumps(asdict(args), indent=2)) - get_torch_device().set_device(f"{get_device_type()}:{args.train.local_rank}") - helper.set_seed(args.train.seed, args.train.enable_full_determinism) - if args.train.local_rank == 0: - helper.enable_third_party_logging() - - if args.train.global_rank == 0: - save_args(args, args.train.output_dir) - - Checkpointer = build_checkpointer(dist_backend=args.train.data_parallel_mode, ckpt_manager=args.train.ckpt_manager) - - init_parallel_state( - dp_size=args.train.data_parallel_size, - dp_replicate_size=args.train.data_parallel_replicate_size, - dp_shard_size=args.train.data_parallel_shard_size, - tp_size=args.train.tensor_parallel_size, - ep_size=args.train.expert_parallel_size, - pp_size=args.train.pipeline_parallel_size, - cp_size=args.train.context_parallel_size, - ulysses_size=args.train.ulysses_parallel_size, - dp_mode=args.train.data_parallel_mode, - ) - - logger.info_rank0("Prepare data") - tokenizer = build_tokenizer(args.model.tokenizer_path) - if args.data.data_type == "conversation": - if not tokenizer.chat_template: - raise ValueError(f"No chat template found in the tokenizer.") - - transform = partial( - process_mdm_sft_example, - tokenizer=tokenizer, - max_seq_len=args.data.max_seq_len, - text_keys=args.data.text_keys, - noise_range=(args.data.noise_range_low, args.data.noise_range_high), - mask_token_id=156895, - ) - elif args.data.data_type == "tokenid": - transform = partial( - process_mdm_tokenized_example, - max_seq_len=args.data.max_seq_len, - text_keys=args.data.text_keys, - noise_range=(args.data.noise_range_low, args.data.noise_range_high), - mask_token_id=156895, - ) - else: - raise NotImplementedError(f"Unsupported data type: {args.data.data_type}.") - - if args.data.dataloader_type == "native": - if args.data.datasets_type == "iterable": - logger.info_rank0("Start building iterative dataset") - train_dataset = build_iterative_dataset(args.data.train_path, transform=transform, seed=args.train.seed) - elif args.data.datasets_type == "mapping": - logger.info_rank0("Start building mapping dataset") - train_dataset = build_mapping_dataset(args.data.train_path, transform=transform) - elif args.data.datasets_type == "local": - logger.info_rank0("Start building local dataset") - train_dataset = build_local_dataset(args.data.train_path, transform=transform) - - dataset_length = None if not hasattr(train_dataset, "__len__") else len(train_dataset) - if args.data.datasets_type == "mapping" or args.data.datasets_type == "local": - dataset_length = dataset_length / args.train.data_parallel_size - args.train.compute_train_steps(args.data.max_seq_len, args.data.train_size, dataset_length) - - train_dataloader = build_dataloader( - dataset=train_dataset, - micro_batch_size=args.train.micro_batch_size, - global_batch_size=args.train.global_batch_size, - dataloader_batch_size=args.train.dataloader_batch_size, - seed=args.train.seed, - max_seq_len=args.data.max_seq_len, - train_steps=args.train.train_steps, - rmpad=args.train.rmpad, - rmpad_with_pos_ids=args.train.rmpad_with_pos_ids, - bsz_warmup_ratio=args.train.bsz_warmup_ratio, - bsz_warmup_init_mbtoken=args.train.bsz_warmup_init_mbtoken, - dyn_bsz_margin=args.train.dyn_bsz_margin, - dyn_bsz_buffer_size=args.train.dyn_bsz_buffer_size, - num_workers=args.data.num_workers, - drop_last=args.data.drop_last, - pin_memory=args.data.pin_memory, - prefetch_factor=args.data.prefetch_factor, - ) - else: - raise NotImplementedError(f"Unsupported dataloader type: {args.data.dataloader_type}.") - - logger.info_rank0("Prepare model") - model = build_foundation_model( - config_path=args.model.config_path, - weights_path=args.model.model_path, - torch_dtype="float32" if args.train.enable_mixed_precision else "bfloat16", - attn_implementation=args.model.attn_implementation, - moe_implementation=args.model.moe_implementation, - init_device=args.train.init_device, - force_use_huggingface=args.model.force_use_huggingface, - ) - model_config = model.config - helper.print_device_mem_info("VRAM usage after building model") - - get_optimizer_pre_hook = getattr(model, "get_optimizer_pre_hook", None) - model = build_parallelize_model( - model, - init_device=args.train.init_device, - weights_path=args.model.model_path, - enable_full_shard=args.train.enable_full_shard, - enable_mixed_precision=args.train.enable_mixed_precision, - enable_gradient_checkpointing=args.train.enable_gradient_checkpointing, - enable_fsdp_offload=args.train.enable_fsdp_offload, - basic_modules=model._no_split_modules + args.model.basic_modules, - enable_reentrant=args.train.enable_reentrant, - enable_forward_prefetch=args.train.enable_forward_prefetch, - broadcast_model_weights_from_rank0=args.train.broadcast_model_weights_from_rank0 - ) - - optimizer = build_optimizer( - model, - lr=args.train.lr, - betas=(args.train.beta1, args.train.beta2), - weight_decay=args.train.weight_decay, - fused=True, - optimizer_type=args.train.optimizer, - ) - - if get_optimizer_pre_hook is not None: - optimizer_pre_hook = get_optimizer_pre_hook(model, model_config, args.train.data_parallel_mode) - optimizer.register_step_pre_hook(optimizer_pre_hook) - - lr_scheduler = build_lr_scheduler( - optimizer, - train_steps=args.train.train_steps * args.train.num_train_epochs, - lr=args.train.lr, - lr_min=args.train.lr_min, - lr_decay_style=args.train.lr_decay_style, - lr_decay_ratio=args.train.lr_decay_ratio, - lr_warmup_ratio=args.train.lr_warmup_ratio, - lr_start=args.train.lr_start, - ) - - if args.train.global_rank == 0: - if args.train.use_wandb: - wandb.init( - project=args.train.wandb_project, - name=args.train.wandb_name, - config={**vars(args.model), **vars(args.data), **vars(args.train)}, # flatten dict - ) - - # save model_assets before training - model_assets = [model_config, tokenizer] - save_model_assets(args.train.model_assets_dir, model_assets) - - if args.train.profile_this_rank: - profiler = helper.create_profiler( - start_step=args.train.profile_start_step, - end_step=args.train.profile_end_step, - trace_dir=args.train.profile_trace_dir, - record_shapes=args.train.profile_record_shapes, - profile_memory=args.train.profile_profile_memory, - with_stack=args.train.profile_with_stack, - global_rank=args.train.global_rank, - ) - profiler.start() - - start_epoch, start_step, global_step = 0, 0, 0 - save_checkpoint_path = None - environ_meter = helper.EnvironMeter( - config=model_config, - global_batch_size=args.train.global_batch_size, - rmpad=args.train.rmpad, - rmpad_with_pos_ids=args.train.rmpad_with_pos_ids, - empty_cache_steps=args.train.empty_cache_steps, - enable_multisource=args.data.enable_multisource, - dataloader=train_dataloader, - data_path=args.data.train_path, - ) - - if args.train.load_checkpoint_path: - state = {"model": model, "optimizer": optimizer, "extra_state": {}} # cannot be None - Checkpointer.load(args.train.load_checkpoint_path, state) - global_step = state["extra_state"]["global_step"] - start_epoch = global_step // args.train.train_steps - start_step = global_step % args.train.train_steps - lr_scheduler.load_state_dict(state["extra_state"]["lr_scheduler"]) - train_dataloader.load_state_dict(state["extra_state"]["train_dataloader"]) - environ_meter.load_state_dict(state["extra_state"]["environ_meter"]) - torch.set_rng_state(state["extra_state"]["torch_rng_state"]) - if start_step == 0: # resume at the end of epoch - iter(train_dataloader) # clear resume state and prefetch data - - dist.barrier() - logger.info_rank0(f"Load distributed checkpoint from {args.train.load_checkpoint_path} successfully!") - - # Build block diffusion attention mask - if args.train.block_diffusion_mode: - bd_attn_full_len = args.data.max_seq_len * 2 - block_size = args.train.block_size - # NOTE: Boolean dtype block diffusion attention mask - block_diffusion_attn_mask_flag = block_diffusion_mask( - b=None, h=None, - q_idx=torch.arange(bd_attn_full_len)[:, None], - kv_idx=torch.arange(bd_attn_full_len)[None, :], - block_size=block_size, - n=args.data.max_seq_len - ).unsqueeze(0).unsqueeze(0) - - block_diffusion_attn_mask_prototype = torch.zeros_like( - block_diffusion_attn_mask_flag, - dtype=torch.float32 if args.train.enable_mixed_precision else torch.bfloat16 - ) - block_diffusion_attn_mask_prototype.masked_fill_(block_diffusion_attn_mask_flag.logical_not(), float("-inf")) - - helper.empty_cache() - model_fwd_context, model_bwd_context = build_activation_offloading_context( - args.train.enable_activation_offload, args.train.enable_gradient_checkpointing, args.train.activation_gpu_limit - ) - model.train() - logger.info( - f"rank{args.train.local_rank} Start training, train_steps: {args.train.train_steps}, epochs: {args.train.num_train_epochs}" - ) - for epoch in range(start_epoch, args.train.num_train_epochs): - if hasattr(train_dataloader, "set_epoch"): - train_dataloader.set_epoch(epoch) - - data_loader_tqdm = trange( - args.train.train_steps, - desc=f"Epoch {epoch + 1}/{args.train.num_train_epochs}", - total=args.train.train_steps, - initial=start_step, - disable=args.train.local_rank != 0, - ) - data_iterator = iter(train_dataloader) - for _ in range(start_step, args.train.train_steps): - global_step += 1 - - try: - micro_batches: List[Dict[str, Any]] = next(data_iterator) - except StopIteration: - logger.info(f"epoch:{epoch} Dataloader finished with drop_last {args.data.drop_last}") - break - - if global_step == 1: - helper.print_example(example=micro_batches[0], rank=args.train.local_rank) - - total_loss = 0 - synchronize() - start_time = time.time() - for micro_batch in micro_batches: - environ_meter.add(micro_batch) - if args.data.enable_multisource: - micro_batch.pop("ds_idx", None) - micro_batch.pop("source_name", None) - - if args.train.block_diffusion_mode: - noisy_input_ids = micro_batch["noisy_input_ids"] - clean_input_ids = micro_batch["input_ids"] - batch_size = noisy_input_ids.shape[0] - full_input_ids = torch.cat([noisy_input_ids, clean_input_ids], dim=1) - noisy_position_ids = torch.arange(noisy_input_ids.shape[1], device=get_device_type(), dtype=torch.long) - clean_position_ids = torch.arange(clean_input_ids.shape[1], device=get_device_type(), dtype=torch.long) - position_ids = torch.cat([noisy_position_ids, clean_position_ids], dim=0).unsqueeze(0).expand(batch_size, -1).clone() - micro_batch["input_ids"] = full_input_ids - micro_batch["position_ids"] = position_ids - micro_batch["attention_mask"] = block_diffusion_attn_mask_prototype.expand(batch_size, -1, -1, -1) - else: - micro_batch["attention_mask"] = None - - micro_batch = { - k: v.to(get_device_type(), non_blocking=True) if isinstance(v, torch.Tensor) else v - for k, v in micro_batch.items() - } - - labels = micro_batch.pop("labels", None) - - with model_fwd_context: - logits: "torch.Tensor" = model(**micro_batch, use_cache=False, output_router_logits=False).logits - if args.train.block_diffusion_mode: - noisy_logits = logits[:, :noisy_input_ids.shape[1]].contiguous() - else: - noisy_logits = logits - - if args.train.same_token_labels: - unscaled_loss = torch.nn.functional.cross_entropy( - noisy_logits.view(-1, noisy_logits.shape[-1]), - labels.view(-1), - reduction="none", - ) - loss = unscaled_loss.sum() / (labels != -100).sum() / len(micro_batches) - else: - shifted_noisy_logits = noisy_logits[:, :-1, :].contiguous() - shifted_labels = labels[:, 1:].contiguous() - unscaled_loss = torch.nn.functional.cross_entropy( - shifted_noisy_logits.view(-1, shifted_noisy_logits.shape[-1]), - shifted_labels.view(-1), - reduction="none", - ).view(shifted_noisy_logits.shape[0], -1) - loss = unscaled_loss.sum() / (shifted_labels != -100).sum() / len(micro_batches) - - with model_bwd_context: - loss.backward() - - total_loss += loss.item() - del micro_batch - - # Prefer model-provided clip_grad_norm_ (now both FSDP1 and FSDP2 registers custom grad norm clipping) - if hasattr(model, "clip_grad_norm_"): - _gn = model.clip_grad_norm_(args.train.max_grad_norm) - grad_norm = _gn.item() if hasattr(_gn, "item") else float(_gn) - else: - logger.info_rank0( - "Can NOT find regitsered clip_grad_norm_ method in the model, using PyTorch default implementation.." - ) - grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.train.max_grad_norm) - - optimizer.step() - lr_scheduler.step() - optimizer.zero_grad() - if hasattr(grad_norm, "full_tensor"): - grad_norm = grad_norm.full_tensor().item() - - # collect mean loss across data parallel group - total_loss, grad_norm = all_reduce((total_loss, grad_norm), group=get_parallel_state().fsdp_group) - synchronize() - delta_time = time.time() - start_time - lr = max(lr_scheduler.get_last_lr()) - train_metrics = environ_meter.step(delta_time, global_step=global_step) - - data_loader_tqdm.set_postfix_str(f"loss: {total_loss:.2f}, grad_norm: {grad_norm:.2f}, lr: {lr:.2e}") - data_loader_tqdm.update() - - if args.train.global_rank == 0: - if args.train.use_wandb: - train_metrics.update( - {"training/loss": total_loss, "training/grad_norm": grad_norm, "training/lr": lr} - ) - wandb.log(train_metrics, step=global_step) - - if args.train.profile_this_rank and global_step <= args.train.profile_end_step: - profiler.step() - if global_step == args.train.profile_end_step: - profiler.stop() - - if args.train.save_steps and global_step % args.train.save_steps == 0: - helper.empty_cache() - save_checkpoint_path = os.path.join(args.train.save_checkpoint_path, f"global_step_{global_step}") - state = { - "model": model, - "optimizer": optimizer, - "extra_state": { - "global_step": global_step, - "lr_scheduler": lr_scheduler.state_dict(), - "train_dataloader": train_dataloader.state_dict(), - "environ_meter": environ_meter.state_dict(), - "torch_rng_state": torch.get_rng_state(), - }, - } - Checkpointer.save(args.train.save_checkpoint_path, state, global_steps=global_step) - - dist.barrier() - logger.info_rank0(f"Distributed checkpoint saved at {save_checkpoint_path} successfully!") - - data_loader_tqdm.close() - start_step = 0 - helper.print_device_mem_info(f"VRAM usage after epoch {epoch + 1}") - if args.train.save_epochs and (epoch + 1) % args.train.save_epochs == 0: - helper.empty_cache() - save_checkpoint_path = os.path.join(args.train.save_checkpoint_path, f"global_step_{global_step}") - state = { - "model": model, - "optimizer": optimizer, - "extra_state": { - "global_step": global_step, - "lr_scheduler": lr_scheduler.state_dict(), - "train_dataloader": train_dataloader.state_dict(), - "environ_meter": environ_meter.state_dict(), - "torch_rng_state": torch.get_rng_state(), - }, - } - Checkpointer.save(args.train.save_checkpoint_path, state, global_steps=global_step) - dist.barrier() - logger.info_rank0(f"Distributed checkpoint saved at {save_checkpoint_path} successfully!") - - synchronize() - # release memory - del optimizer, lr_scheduler - helper.empty_cache() - # save model in huggingface's format - if args.train.global_rank == 0 and args.train.save_hf_weights and save_checkpoint_path is not None: - hf_weights_path = os.path.join(save_checkpoint_path, "hf_ckpt") - model_state_dict = ckpt_to_state_dict( - save_checkpoint_path=save_checkpoint_path, - output_dir=args.train.output_dir, - ckpt_manager=args.train.ckpt_manager, - ) - save_model_weights(hf_weights_path, model_state_dict, model_assets=model_assets) - logger.info_rank0(f"Huggingface checkpoint saved at {hf_weights_path} successfully!") - - dist.barrier() - dist.destroy_process_group() + run_llada2_training(LLaDA2Arguments) if __name__ == "__main__": diff --git a/tasks/train_llada2_bd_with_dparallel.py b/tasks/train_llada2_bd_with_dparallel.py index 2084cc5..f00a321 100644 --- a/tasks/train_llada2_bd_with_dparallel.py +++ b/tasks/train_llada2_bd_with_dparallel.py @@ -1,623 +1,9 @@ -import json -import os -import time -from dataclasses import asdict, dataclass, field -from functools import partial -from typing import Any, Dict, List, Literal, Tuple, Optional +from train_llada2_common import LLaDA2Arguments, run_llada2_training -import torch -import torch.nn.functional as F -import torch.distributed as dist -import wandb -from tqdm import trange -from veomni.checkpoint import build_checkpointer, ckpt_to_state_dict -from veomni.data import ( - build_dataloader, - build_iterative_dataset, - build_mapping_dataset, -) -from veomni.distributed.offloading import build_activation_offloading_context -from veomni.distributed.parallel_state import get_parallel_state, init_parallel_state -from veomni.distributed.torch_parallelize import build_parallelize_model -from veomni.models import build_foundation_model, build_tokenizer, save_model_assets, save_model_weights -from veomni.optim import build_lr_scheduler, build_optimizer -from veomni.utils import helper -from veomni.utils.arguments import DataArguments, ModelArguments, TrainingArguments, parse_args, save_args -from veomni.utils.device import ( - get_device_type, - get_nccl_backend, - get_torch_device, - synchronize, -) -from veomni.utils.dist_utils import all_reduce -from veomni.models.registry import ModelRegistry -ModelRegistry.register_modeling_path("models.llada2_moe") -from dataset.data_transform import process_mdm_tokenized_example, process_mdm_sft_example -from dataset import build_local_dataset - - -logger = helper.create_logger(__name__) - -@dataclass -class LLaDA2ModelArguments(ModelArguments): - attn_implementation: Optional[Literal["eager", "sdpa", "flex_attention"]] = field( - default="sdpa", - metadata={"help": "Attention implementation to use."}, - ) - - -@dataclass -class LLaDA2DataArguments(DataArguments): - data_type: Literal["conversation", "tokenid"] = field( - default="conversation", - metadata={"help": "Type of the training data."}, - ) - datasets_type: Literal["mapping", "local"] = field( - default="mapping", - metadata={"help": "Type of the datasets."}, - ) - text_keys: str = field( - default="messages", - metadata={"help": "Key to get text from the training data."}, - ) - noise_range_low: float = field( - default=0.3, - metadata={"help": "Noise level for random flip input_ids to mask_ids"} - ) - noise_range_high: float = field( - default=0.8, - metadata={"help": "Noise level for random flip input_ids to mask_ids"} - ) - - def __post_init__(self): - super().__post_init__() - if self.noise_range_low > self.noise_range_high: - raise ValueError( - f"noise_range_low ({self.noise_range_low}) " - f"cannot be greater than noise_range_high ({self.noise_range_high})." - ) - - if not (0.0 <= self.noise_range_low <= 1.0): - raise ValueError( - f"noise_range_low must be between 0.0 and 1.0, but got {self.noise_range_low}." - ) - - if not (0.0 <= self.noise_range_high <= 1.0): - raise ValueError( - f"noise_range_high must be between 0.0 and 1.0, but got {self.noise_range_high}." - ) - - -@dataclass -class LLaDA2TrainingArguments(TrainingArguments): - beta1: float = field( - default=0.9, - metadata={"help": "AdamW optimizer beta1."}, - ) - beta2: float = field( - default=0.999, - metadata={"help": "AdamW optimizer beta2"}, - ) - confidence_beta: float = field( - default=0.0, - metadata={"help": "Weight for the confidence loss entropy of correct predictions. Set to 0 to disable."}, - ) - block_diffusion_mode: bool = field( - default=False, - metadata={"help": "If train MDM in block_diffusion mode. True: use block_diffusion, False: full_attention"} - ) - block_size: int = field( - default=32, - metadata={"help": "The block size for block diffusion block size"} - ) - same_token_labels: bool = field( - default=False, - metadata={"help": "If use same token location labels. True: no shift, False: use next-token prediction shift."} - ) - - -@dataclass -class Arguments: - model: "LLaDA2ModelArguments" = field(default_factory=LLaDA2ModelArguments) - data: "LLaDA2DataArguments" = field(default_factory=LLaDA2DataArguments) - train: "LLaDA2TrainingArguments" = field(default_factory=LLaDA2TrainingArguments) - - -def block_diffusion_mask(b, h, q_idx, kv_idx, block_size=None, n=None): - """ - Constructs the specialized block diffusion attention mask for training - composed of three masks: - - **Block Diagonal Mask (M_BD)**: Self-attention within noised blocks - - **Offset Block Causal Mask (M_OBC)**: Cross-attention for conditional context - - **Block Causal Mask (M_BC)**: Attention to update x0 - - Args: - b, h: Batch and head indices (ignored for mask logic). - q_idx, kv_idx: Query and Key indices. - seq_len: Total sequence length. - block_size: Defines the block structure. - - Returns: - A boolean attention mask. - """ - - # Indicate whether token belongs to xt or x0 - x0_flag_q = (q_idx >= n) - x0_flag_kv = (kv_idx >= n) - - # Compute block indices - block_q = torch.where(x0_flag_q == 1, - (q_idx - n) // block_size, - q_idx // block_size) - block_kv = torch.where(x0_flag_kv == 1, - (kv_idx - n) // block_size, - kv_idx // block_size) - - # **1. Block Diagonal Mask (M_BD) ** - block_diagonal = (block_q == block_kv) & (x0_flag_q == x0_flag_kv) - - # **2. Offset Block-Causal Mask (M_OBC) ** - offset_block_causal = ( - (block_q > block_kv) - & (x0_flag_kv == 1) - & (x0_flag_q == 0) - ) - - # **3. Block-Causal Mask (M_BC) ** - block_causal = (block_q >= block_kv) & (x0_flag_kv == 1) & (x0_flag_q == 1) - - # **4. Combine Masks ** - return block_diagonal | offset_block_causal | block_causal - -def compute_confidence_loss(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor: - """ - Calculate the average entropy of the output distribution at positions where the model predicts correctly. - Args: - logits (torch.Tensor): The raw output logits from the model, with shape (batch_size, seq_len, vocab_size). - labels (torch.Tensor): The ground truth labels, with shape (batch_size, seq_len). -100 indicates positions to be ignored. - Returns: - torch.Tensor: A scalar tensor representing the confidence loss. Returns 0 if there are no correct predictions. - """ - labels = labels.to(logits.device) - - valid_mask = (labels != -100) - if not valid_mask.any(): - return torch.tensor(0.0, device=logits.device) - - predicted_tokens = torch.argmax(logits, dim=-1) - - correct_mask = (predicted_tokens == labels) & valid_mask - - if correct_mask.sum() == 0: - return torch.tensor(0.0, device=logits.device) - - log_probs = F.log_softmax(logits, dim=-1) - probs = torch.exp(log_probs) - entropy_per_token = -torch.sum(probs * log_probs, dim=-1) - - entropy_at_correct_positions = entropy_per_token[correct_mask] - - confidence_loss = entropy_at_correct_positions.mean() - - return confidence_loss - def main(): - dist.init_process_group(backend=get_nccl_backend()) - args = parse_args(Arguments) - logger.info(f"Process rank: {args.train.global_rank}, world size: {args.train.world_size}") - logger.info_rank0(json.dumps(asdict(args), indent=2)) - get_torch_device().set_device(f"{get_device_type()}:{args.train.local_rank}") - helper.set_seed(args.train.seed, args.train.enable_full_determinism) - if args.train.local_rank == 0: - helper.enable_third_party_logging() - - if args.train.global_rank == 0: - save_args(args, args.train.output_dir) - - Checkpointer = build_checkpointer(dist_backend=args.train.data_parallel_mode, ckpt_manager=args.train.ckpt_manager) - - init_parallel_state( - dp_size=args.train.data_parallel_size, - dp_replicate_size=args.train.data_parallel_replicate_size, - dp_shard_size=args.train.data_parallel_shard_size, - tp_size=args.train.tensor_parallel_size, - ep_size=args.train.expert_parallel_size, - pp_size=args.train.pipeline_parallel_size, - cp_size=args.train.context_parallel_size, - ulysses_size=args.train.ulysses_parallel_size, - dp_mode=args.train.data_parallel_mode, - ) - - logger.info_rank0("Prepare data") - tokenizer = build_tokenizer(args.model.tokenizer_path) - if args.data.data_type == "conversation": - if not tokenizer.chat_template: - raise ValueError(f"No chat template found in the tokenizer.") - - transform = partial( - process_mdm_sft_example, - tokenizer=tokenizer, - max_seq_len=args.data.max_seq_len, - text_keys=args.data.text_keys, - noise_range=(args.data.noise_range_low, args.data.noise_range_high), - mask_token_id=156895, - ) - elif args.data.data_type == "tokenid": - transform = partial( - process_mdm_tokenized_example, - max_seq_len=args.data.max_seq_len, - text_keys=args.data.text_keys, - noise_range=(args.data.noise_range_low, args.data.noise_range_high), - mask_token_id=156895, - ) - else: - raise NotImplementedError(f"Unsupported data type: {args.data.data_type}.") - - if args.data.dataloader_type == "native": - if args.data.datasets_type == "iterable": - logger.info_rank0("Start building iterative dataset") - train_dataset = build_iterative_dataset(args.data.train_path, transform=transform, seed=args.train.seed) - elif args.data.datasets_type == "mapping": - logger.info_rank0("Start building mapping dataset") - train_dataset = build_mapping_dataset(args.data.train_path, transform=transform) - elif args.data.datasets_type == "local": - logger.info_rank0("Start building local dataset") - train_dataset = build_local_dataset(args.data.train_path, transform=transform) - - dataset_length = None if not hasattr(train_dataset, "__len__") else len(train_dataset) - if args.data.datasets_type == "mapping" or args.data.datasets_type == "local": - dataset_length = dataset_length / args.train.data_parallel_size - args.train.compute_train_steps(args.data.max_seq_len, args.data.train_size, dataset_length) - - train_dataloader = build_dataloader( - dataset=train_dataset, - micro_batch_size=args.train.micro_batch_size, - global_batch_size=args.train.global_batch_size, - dataloader_batch_size=args.train.dataloader_batch_size, - seed=args.train.seed, - max_seq_len=args.data.max_seq_len, - train_steps=args.train.train_steps, - rmpad=args.train.rmpad, - rmpad_with_pos_ids=args.train.rmpad_with_pos_ids, - bsz_warmup_ratio=args.train.bsz_warmup_ratio, - bsz_warmup_init_mbtoken=args.train.bsz_warmup_init_mbtoken, - dyn_bsz_margin=args.train.dyn_bsz_margin, - dyn_bsz_buffer_size=args.train.dyn_bsz_buffer_size, - num_workers=args.data.num_workers, - drop_last=args.data.drop_last, - pin_memory=args.data.pin_memory, - prefetch_factor=args.data.prefetch_factor, - ) - else: - raise NotImplementedError(f"Unsupported dataloader type: {args.data.dataloader_type}.") - - logger.info_rank0("Prepare model") - model = build_foundation_model( - config_path=args.model.config_path, - weights_path=args.model.model_path, - torch_dtype="float32" if args.train.enable_mixed_precision else "bfloat16", - attn_implementation=args.model.attn_implementation, - moe_implementation=args.model.moe_implementation, - init_device=args.train.init_device, - force_use_huggingface=args.model.force_use_huggingface, - ) - model_config = model.config - helper.print_device_mem_info("VRAM usage after building model") - - get_optimizer_pre_hook = getattr(model, "get_optimizer_pre_hook", None) - model = build_parallelize_model( - model, - init_device=args.train.init_device, - weights_path=args.model.model_path, - enable_full_shard=args.train.enable_full_shard, - enable_mixed_precision=args.train.enable_mixed_precision, - enable_gradient_checkpointing=args.train.enable_gradient_checkpointing, - enable_fsdp_offload=args.train.enable_fsdp_offload, - basic_modules=model._no_split_modules + args.model.basic_modules, - enable_reentrant=args.train.enable_reentrant, - enable_forward_prefetch=args.train.enable_forward_prefetch, - broadcast_model_weights_from_rank0=args.train.broadcast_model_weights_from_rank0 - ) - - optimizer = build_optimizer( - model, - lr=args.train.lr, - betas=(args.train.beta1, args.train.beta2), - weight_decay=args.train.weight_decay, - fused=True, - optimizer_type=args.train.optimizer, - ) - - if get_optimizer_pre_hook is not None: - optimizer_pre_hook = get_optimizer_pre_hook(model, model_config, args.train.data_parallel_mode) - optimizer.register_step_pre_hook(optimizer_pre_hook) - - lr_scheduler = build_lr_scheduler( - optimizer, - train_steps=args.train.train_steps * args.train.num_train_epochs, - lr=args.train.lr, - lr_min=args.train.lr_min, - lr_decay_style=args.train.lr_decay_style, - lr_decay_ratio=args.train.lr_decay_ratio, - lr_warmup_ratio=args.train.lr_warmup_ratio, - lr_start=args.train.lr_start, - ) - - if args.train.global_rank == 0: - if args.train.use_wandb: - wandb.init( - project=args.train.wandb_project, - name=args.train.wandb_name, - config={**vars(args.model), **vars(args.data), **vars(args.train)}, # flatten dict - ) - - # save model_assets before training - model_assets = [model_config, tokenizer] - save_model_assets(args.train.model_assets_dir, model_assets) - - if args.train.profile_this_rank: - profiler = helper.create_profiler( - start_step=args.train.profile_start_step, - end_step=args.train.profile_end_step, - trace_dir=args.train.profile_trace_dir, - record_shapes=args.train.profile_record_shapes, - profile_memory=args.train.profile_profile_memory, - with_stack=args.train.profile_with_stack, - global_rank=args.train.global_rank, - ) - profiler.start() - - start_epoch, start_step, global_step = 0, 0, 0 - save_checkpoint_path = None - environ_meter = helper.EnvironMeter( - config=model_config, - global_batch_size=args.train.global_batch_size, - rmpad=args.train.rmpad, - rmpad_with_pos_ids=args.train.rmpad_with_pos_ids, - empty_cache_steps=args.train.empty_cache_steps, - enable_multisource=args.data.enable_multisource, - dataloader=train_dataloader, - data_path=args.data.train_path, - ) - - if args.train.load_checkpoint_path: - state = {"model": model, "optimizer": optimizer, "extra_state": {}} # cannot be None - Checkpointer.load(args.train.load_checkpoint_path, state) - global_step = state["extra_state"]["global_step"] - start_epoch = global_step // args.train.train_steps - start_step = global_step % args.train.train_steps - lr_scheduler.load_state_dict(state["extra_state"]["lr_scheduler"]) - train_dataloader.load_state_dict(state["extra_state"]["train_dataloader"]) - environ_meter.load_state_dict(state["extra_state"]["environ_meter"]) - torch.set_rng_state(state["extra_state"]["torch_rng_state"]) - if start_step == 0: # resume at the end of epoch - iter(train_dataloader) # clear resume state and prefetch data - - dist.barrier() - logger.info_rank0(f"Load distributed checkpoint from {args.train.load_checkpoint_path} successfully!") - - # Build block diffusion attention mask - if args.train.block_diffusion_mode: - bd_attn_full_len = args.data.max_seq_len * 2 - block_size = args.train.block_size - # NOTE: Boolean dtype block diffusion attention mask - block_diffusion_attn_mask_flag = block_diffusion_mask( - b=None, h=None, - q_idx=torch.arange(bd_attn_full_len)[:, None], - kv_idx=torch.arange(bd_attn_full_len)[None, :], - block_size=block_size, - n=args.data.max_seq_len - ).unsqueeze(0).unsqueeze(0) - - block_diffusion_attn_mask_prototype = torch.zeros_like( - block_diffusion_attn_mask_flag, - dtype=torch.float32 if args.train.enable_mixed_precision else torch.bfloat16 - ) - block_diffusion_attn_mask_prototype.masked_fill_(block_diffusion_attn_mask_flag.logical_not(), float("-inf")) - - helper.empty_cache() - model_fwd_context, model_bwd_context = build_activation_offloading_context( - args.train.enable_activation_offload, args.train.enable_gradient_checkpointing, args.train.activation_gpu_limit - ) - model.train() - logger.info( - f"rank{args.train.local_rank} Start training, train_steps: {args.train.train_steps}, epochs: {args.train.num_train_epochs}" - ) - for epoch in range(start_epoch, args.train.num_train_epochs): - if hasattr(train_dataloader, "set_epoch"): - train_dataloader.set_epoch(epoch) - - data_loader_tqdm = trange( - args.train.train_steps, - desc=f"Epoch {epoch + 1}/{args.train.num_train_epochs}", - total=args.train.train_steps, - initial=start_step, - disable=args.train.local_rank != 0, - ) - data_iterator = iter(train_dataloader) - for _ in range(start_step, args.train.train_steps): - global_step += 1 - - try: - micro_batches: List[Dict[str, Any]] = next(data_iterator) - except StopIteration: - logger.info(f"epoch:{epoch} Dataloader finished with drop_last {args.data.drop_last}") - break - - if global_step == 1: - helper.print_example(example=micro_batches[0], rank=args.train.local_rank) - - total_loss = 0 - synchronize() - start_time = time.time() - num_accumulation_steps = len(micro_batches) - total_consistency_loss = 0 - total_confidence_loss = 0 - - for micro_batch in micro_batches: - environ_meter.add(micro_batch) - if args.data.enable_multisource: - micro_batch.pop("ds_idx", None) - micro_batch.pop("source_name", None) - - micro_batch = { - k: v.to(get_device_type(), non_blocking=True) if isinstance(v, torch.Tensor) else v - for k, v in micro_batch.items() - } - if args.train.block_diffusion_mode: - noisy_input_ids = micro_batch["noisy_input_ids"] - clean_input_ids = micro_batch["input_ids"] - batch_size = noisy_input_ids.shape[0] - full_input_ids = torch.cat([noisy_input_ids, clean_input_ids], dim=1) - noisy_position_ids = torch.arange(noisy_input_ids.shape[1], device=get_device_type(), dtype=torch.long) - clean_position_ids = torch.arange(clean_input_ids.shape[1], device=get_device_type(), dtype=torch.long) - position_ids = torch.cat([noisy_position_ids, clean_position_ids], dim=0).unsqueeze(0).expand(batch_size, -1).clone() - micro_batch["input_ids"] = full_input_ids - micro_batch["position_ids"] = position_ids - micro_batch["attention_mask"] = block_diffusion_attn_mask_prototype.expand(batch_size, -1, -1, -1) - else: - micro_batch["attention_mask"] = None - - labels = micro_batch.pop("labels", None) - - with model_fwd_context: - logits: "torch.Tensor" = model(**micro_batch, use_cache=False, output_router_logits=False).logits - if args.train.block_diffusion_mode: - noisy_logits = logits[:, :noisy_input_ids.shape[1]].contiguous() - else: - noisy_logits = logits - - confidence_loss = torch.tensor(0.0, device=noisy_logits.device) - if args.train.confidence_beta > 0: - confidence_loss = compute_confidence_loss( - logits=noisy_logits, - labels=labels, - ) - - if args.train.same_token_labels: - unscaled_loss = torch.nn.functional.cross_entropy( - noisy_logits.view(-1, noisy_logits.shape[-1]), - labels.view(-1), - reduction="none", - ).view(noisy_logits.shape[0], -1) - consistency_loss = unscaled_loss.sum() / (labels != -100).sum() - else: - shifted_noisy_logits = noisy_logits[:, :-1, :].contiguous() - shifted_labels = labels[:, 1:].contiguous() - unscaled_loss = torch.nn.functional.cross_entropy( - shifted_noisy_logits.view(-1, shifted_noisy_logits.shape[-1]), - shifted_labels.view(-1), - reduction="none", - ).view(shifted_noisy_logits.shape[0], -1) - consistency_loss = unscaled_loss.sum() / (shifted_labels != -100).sum() - - combined_loss = consistency_loss + confidence_loss * args.train.confidence_beta - loss = combined_loss / num_accumulation_steps - with model_bwd_context: - loss.backward() - - total_loss += loss.item() - total_consistency_loss += consistency_loss.item() / num_accumulation_steps - total_confidence_loss += confidence_loss.item() / num_accumulation_steps - del micro_batch - - # Prefer model-provided clip_grad_norm_ (now both FSDP1 and FSDP2 registers custom grad norm clipping) - if hasattr(model, "clip_grad_norm_"): - _gn = model.clip_grad_norm_(args.train.max_grad_norm) - grad_norm = _gn.item() if hasattr(_gn, "item") else float(_gn) - else: - logger.info_rank0( - "Can NOT find regitsered clip_grad_norm_ method in the model, using PyTorch default implementation.." - ) - grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.train.max_grad_norm) - - optimizer.step() - lr_scheduler.step() - optimizer.zero_grad() - if hasattr(grad_norm, "full_tensor"): - grad_norm = grad_norm.full_tensor().item() - - # collect mean loss across data parallel group - total_loss, grad_norm = all_reduce((total_loss, grad_norm), group=get_parallel_state().fsdp_group) - synchronize() - delta_time = time.time() - start_time - lr = max(lr_scheduler.get_last_lr()) - train_metrics = environ_meter.step(delta_time, global_step=global_step) - - data_loader_tqdm.set_postfix_str(f"loss: {total_loss:.2f}, cons: {total_consistency_loss:.2f}, conf: {total_confidence_loss:.2f}, grad_norm: {grad_norm:.2f}, lr: {lr:.2e}") - data_loader_tqdm.update() - - if args.train.global_rank == 0: - if args.train.use_wandb: - train_metrics.update( - {"training/loss": total_loss, "training/cons_loss": total_consistency_loss, "training/conf_loss": total_confidence_loss, "training/grad_norm": grad_norm, "training/lr": lr} - ) - wandb.log(train_metrics, step=global_step) - - if args.train.profile_this_rank and global_step <= args.train.profile_end_step: - profiler.step() - if global_step == args.train.profile_end_step: - profiler.stop() - - if args.train.save_steps and global_step % args.train.save_steps == 0: - helper.empty_cache() - save_checkpoint_path = os.path.join(args.train.save_checkpoint_path, f"global_step_{global_step}") - state = { - "model": model, - "optimizer": optimizer, - "extra_state": { - "global_step": global_step, - "lr_scheduler": lr_scheduler.state_dict(), - "train_dataloader": train_dataloader.state_dict(), - "environ_meter": environ_meter.state_dict(), - "torch_rng_state": torch.get_rng_state(), - }, - } - Checkpointer.save(args.train.save_checkpoint_path, state, global_steps=global_step) - - dist.barrier() - logger.info_rank0(f"Distributed checkpoint saved at {save_checkpoint_path} successfully!") - - data_loader_tqdm.close() - start_step = 0 - helper.print_device_mem_info(f"VRAM usage after epoch {epoch + 1}") - if args.train.save_epochs and (epoch + 1) % args.train.save_epochs == 0: - helper.empty_cache() - save_checkpoint_path = os.path.join(args.train.save_checkpoint_path, f"global_step_{global_step}") - state = { - "model": model, - "optimizer": optimizer, - "extra_state": { - "global_step": global_step, - "lr_scheduler": lr_scheduler.state_dict(), - "train_dataloader": train_dataloader.state_dict(), - "environ_meter": environ_meter.state_dict(), - "torch_rng_state": torch.get_rng_state(), - }, - } - Checkpointer.save(args.train.save_checkpoint_path, state, global_steps=global_step) - dist.barrier() - logger.info_rank0(f"Distributed checkpoint saved at {save_checkpoint_path} successfully!") - - synchronize() - # release memory - del optimizer, lr_scheduler - helper.empty_cache() - # save model in huggingface's format - if args.train.global_rank == 0 and args.train.save_hf_weights and save_checkpoint_path is not None: - hf_weights_path = os.path.join(save_checkpoint_path, "hf_ckpt") - model_state_dict = ckpt_to_state_dict( - save_checkpoint_path=save_checkpoint_path, - output_dir=args.train.output_dir, - ckpt_manager=args.train.ckpt_manager, - ) - save_model_weights(hf_weights_path, model_state_dict, model_assets=model_assets) - logger.info_rank0(f"Huggingface checkpoint saved at {hf_weights_path} successfully!") - - dist.barrier() - dist.destroy_process_group() + run_llada2_training(LLaDA2Arguments) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tasks/train_llada2_common.py b/tasks/train_llada2_common.py new file mode 100644 index 0000000..a9618af --- /dev/null +++ b/tasks/train_llada2_common.py @@ -0,0 +1,663 @@ +import json +import os +import time +from dataclasses import asdict, dataclass, field +from datetime import timedelta +from functools import partial +from typing import Any, Dict, List, Literal, Optional, Tuple + +import torch +import torch.distributed as dist +import torch.nn.functional as F +import wandb +from torch.utils.checkpoint import set_checkpoint_debug_enabled +from tqdm import trange + +import models.llada2_moe # noqa: F401 - registers LLaDA2 MoE with the VeOmni loader. +from veomni.arguments import DataArguments, ModelArguments, TrainingArguments, VeOmniArguments, parse_args, save_args +from veomni.checkpoint import build_checkpointer +from veomni.data import build_dataloader, build_dataset +from veomni.distributed.clip_grad_norm import veomni_clip_grad_norm +from veomni.distributed.offloading import build_activation_offloading_context +from veomni.distributed.parallel_state import get_parallel_state, init_parallel_state +from veomni.distributed.torch_parallelize import build_parallelize_model +from veomni.models import build_foundation_model, build_tokenizer, save_model_assets +from veomni.optim import build_lr_scheduler, build_optimizer +from veomni.utils import helper +from veomni.utils.device import ( + get_device_type, + get_dist_comm_backend, + get_torch_device, + is_nccl_backend, + synchronize, +) +from veomni.utils.dist_utils import all_reduce +from veomni.utils.save_safetensor_utils import save_hf_safetensor + +try: + from dataset import build_local_dataset + from dataset.data_transform import process_mdm_sft_example, process_mdm_tokenized_example +except ImportError: + from tasks.dataset import build_local_dataset + from tasks.dataset.data_transform import process_mdm_sft_example, process_mdm_tokenized_example + + +logger = helper.create_logger(__name__) + + +@dataclass +class LLaDA2ModelArguments(ModelArguments): + attn_implementation: Optional[Literal["eager", "sdpa", "flex_attention"]] = field( + default=None, + metadata={"help": "Deprecated. Use model.ops_implementation.attn_implementation."}, + ) + moe_implementation: Optional[str] = field( + default=None, + metadata={"help": "Deprecated. Use model.ops_implementation.moe_implementation."}, + ) + + def __post_init__(self): + super().__post_init__() + if self.attn_implementation is not None: + self.ops_implementation.attn_implementation = self.attn_implementation + if self.moe_implementation is not None: + self.ops_implementation.moe_implementation = self.moe_implementation + + +@dataclass +class LLaDA2DataArguments(DataArguments): + data_type: Literal["conversation", "tokenid"] = field( + default="conversation", + metadata={"help": "Type of the training data."}, + ) + datasets_type: Literal["mapping", "iterable", "local"] = field( + default="mapping", + metadata={"help": "Type of the datasets."}, + ) + text_keys: Optional[str] = field( + default=None, + metadata={"help": "Key to get text or token ids from the training data."}, + ) + noise_range_low: float = field( + default=0.3, + metadata={"help": "Lower bound of random mask noise ratio."}, + ) + noise_range_high: float = field( + default=0.8, + metadata={"help": "Upper bound of random mask noise ratio."}, + ) + mask_token_id: int = field( + default=156895, + metadata={"help": "LLaDA2 mask token id."}, + ) + + def __post_init__(self): + if self.text_keys is None: + self.text_keys = "input_ids" if self.data_type == "tokenid" else "messages" + super().__post_init__() + if self.noise_range_low > self.noise_range_high: + raise ValueError( + f"noise_range_low ({self.noise_range_low}) cannot be greater than " + f"noise_range_high ({self.noise_range_high})." + ) + if not (0.0 <= self.noise_range_low <= 1.0): + raise ValueError(f"noise_range_low must be between 0.0 and 1.0, but got {self.noise_range_low}.") + if not (0.0 <= self.noise_range_high <= 1.0): + raise ValueError(f"noise_range_high must be between 0.0 and 1.0, but got {self.noise_range_high}.") + + +@dataclass +class LLaDA2TrainingArguments(TrainingArguments): + beta1: float = field( + default=0.9, + metadata={"help": "AdamW optimizer beta1."}, + ) + beta2: float = field( + default=0.999, + metadata={"help": "AdamW optimizer beta2."}, + ) + confidence_beta: float = field( + default=0.0, + metadata={"help": "Weight for the confidence loss entropy of correct predictions. Set to 0 to disable."}, + ) + block_diffusion_mode: bool = field( + default=False, + metadata={"help": "Train MDM in block diffusion mode."}, + ) + block_size: int = field( + default=32, + metadata={"help": "Block size for block diffusion."}, + ) + same_token_labels: bool = field( + default=False, + metadata={"help": "Use same token labels instead of next-token shifted labels."}, + ) + + +@dataclass +class LLaDA2Arguments(VeOmniArguments): + model: LLaDA2ModelArguments = field(default_factory=LLaDA2ModelArguments) + data: LLaDA2DataArguments = field(default_factory=LLaDA2DataArguments) + train: LLaDA2TrainingArguments = field(default_factory=LLaDA2TrainingArguments) + + +def block_diffusion_mask(b, h, q_idx, kv_idx, block_size=None, n=None): + del b, h + x0_flag_q = q_idx >= n + x0_flag_kv = kv_idx >= n + + block_q = torch.where(x0_flag_q == 1, (q_idx - n) // block_size, q_idx // block_size) + block_kv = torch.where(x0_flag_kv == 1, (kv_idx - n) // block_size, kv_idx // block_size) + + block_diagonal = (block_q == block_kv) & (x0_flag_q == x0_flag_kv) + offset_block_causal = (block_q > block_kv) & (x0_flag_kv == 1) & (x0_flag_q == 0) + block_causal = (block_q >= block_kv) & (x0_flag_kv == 1) & (x0_flag_q == 1) + return block_diagonal | offset_block_causal | block_causal + + +def compute_confidence_loss(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor: + labels = labels.to(logits.device) + valid_mask = labels != -100 + if not valid_mask.any(): + return torch.tensor(0.0, device=logits.device) + + predicted_tokens = torch.argmax(logits, dim=-1) + correct_mask = (predicted_tokens == labels) & valid_mask + if correct_mask.sum() == 0: + return torch.tensor(0.0, device=logits.device) + + log_probs = F.log_softmax(logits, dim=-1) + probs = torch.exp(log_probs) + entropy_per_token = -torch.sum(probs * log_probs, dim=-1) + return entropy_per_token[correct_mask].mean() + + +def _build_transform(args: LLaDA2Arguments, tokenizer): + noise_range = (args.data.noise_range_low, args.data.noise_range_high) + if args.data.data_type == "conversation": + if not tokenizer.chat_template: + raise ValueError("No chat template found in the tokenizer.") + return partial( + process_mdm_sft_example, + tokenizer=tokenizer, + max_seq_len=args.data.max_seq_len, + text_keys=args.data.text_keys, + noise_range=noise_range, + mask_token_id=args.data.mask_token_id, + ) + if args.data.data_type == "tokenid": + return partial( + process_mdm_tokenized_example, + max_seq_len=args.data.max_seq_len, + text_keys=args.data.text_keys, + noise_range=noise_range, + mask_token_id=args.data.mask_token_id, + ) + raise NotImplementedError(f"Unsupported data type: {args.data.data_type}.") + + +def _build_train_dataset(args: LLaDA2Arguments, transform): + if args.data.datasets_type == "local": + return build_local_dataset(args.data.train_path, transform=transform, seed=args.train.seed) + + return build_dataset( + dataset_name=args.data.dataset_name, + transform=transform, + dataloader_batch_size=args.train.dataloader_batch_size, + seed=args.train.seed, + **asdict(args.data), + ) + + +def _build_block_diffusion_mask(args: LLaDA2Arguments) -> Optional[torch.Tensor]: + if not args.train.block_diffusion_mode: + return None + + full_len = args.data.max_seq_len * 2 + mask_flag = block_diffusion_mask( + b=None, + h=None, + q_idx=torch.arange(full_len)[:, None], + kv_idx=torch.arange(full_len)[None, :], + block_size=args.train.block_size, + n=args.data.max_seq_len, + ).unsqueeze(0).unsqueeze(0) + + mask_dtype = torch.float32 if args.train.accelerator.fsdp_config.mixed_precision.enable else torch.bfloat16 + mask = torch.zeros_like(mask_flag, dtype=mask_dtype) + mask.masked_fill_(mask_flag.logical_not(), float("-inf")) + return mask + + +def _prepare_micro_batch( + args: LLaDA2Arguments, + micro_batch: Dict[str, Any], + block_diffusion_attn_mask: Optional[torch.Tensor], +) -> Tuple[Dict[str, Any], int]: + if args.train.block_diffusion_mode: + noisy_input_ids = micro_batch.pop("noisy_input_ids") + clean_input_ids = micro_batch["input_ids"] + batch_size = noisy_input_ids.shape[0] + noisy_seq_len = noisy_input_ids.shape[1] + + full_input_ids = torch.cat([noisy_input_ids, clean_input_ids], dim=1) + noisy_position_ids = torch.arange(noisy_seq_len, device=full_input_ids.device, dtype=torch.long) + clean_position_ids = torch.arange(clean_input_ids.shape[1], device=full_input_ids.device, dtype=torch.long) + position_ids = torch.cat([noisy_position_ids, clean_position_ids], dim=0).unsqueeze(0) + + micro_batch["input_ids"] = full_input_ids + micro_batch["position_ids"] = position_ids.expand(batch_size, -1).clone() + micro_batch["attention_mask"] = block_diffusion_attn_mask.expand(batch_size, -1, -1, -1) + else: + noisy_seq_len = micro_batch["input_ids"].shape[1] + micro_batch.pop("noisy_input_ids", None) + micro_batch["attention_mask"] = None + + micro_batch = { + k: v.to(get_device_type(), non_blocking=True) if isinstance(v, torch.Tensor) else v + for k, v in micro_batch.items() + } + return micro_batch, noisy_seq_len + + +def _compute_llada2_loss( + args: LLaDA2Arguments, + noisy_logits: torch.Tensor, + labels: torch.Tensor, + num_micro_steps: int, +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + confidence_loss = torch.tensor(0.0, device=noisy_logits.device) + if args.train.confidence_beta > 0: + confidence_loss = compute_confidence_loss(logits=noisy_logits, labels=labels) + + if args.train.same_token_labels: + unscaled_loss = F.cross_entropy( + noisy_logits.view(-1, noisy_logits.shape[-1]), + labels.view(-1), + reduction="none", + ) + denom = (labels != -100).sum().clamp_min(1) + consistency_loss = unscaled_loss.sum() / denom + else: + shifted_noisy_logits = noisy_logits[:, :-1, :].contiguous() + shifted_labels = labels[:, 1:].contiguous() + unscaled_loss = F.cross_entropy( + shifted_noisy_logits.view(-1, shifted_noisy_logits.shape[-1]), + shifted_labels.view(-1), + reduction="none", + ) + denom = (shifted_labels != -100).sum().clamp_min(1) + consistency_loss = unscaled_loss.sum() / denom + + combined_loss = consistency_loss + confidence_loss * args.train.confidence_beta + return combined_loss / num_micro_steps, consistency_loss, confidence_loss + + +def run_llada2_training(arguments_cls=LLaDA2Arguments): + nccl_timeout = os.getenv("NCCL_TIMEOUT", None) + pg_nccl_timeout = None + if nccl_timeout is not None and is_nccl_backend(): + pg_nccl_timeout = timedelta(seconds=int(nccl_timeout)) + logger.info(f"Process_group timeout: {nccl_timeout}") + dist.init_process_group(backend=get_dist_comm_backend(), timeout=pg_nccl_timeout) + + args = parse_args(arguments_cls) + logger.info(f"Process rank: {args.train.global_rank}, world size: {args.train.world_size}") + logger.info_rank0(json.dumps(asdict(args), indent=2)) + get_torch_device().set_device(f"{get_device_type()}:{args.train.local_rank}") + helper.set_seed(args.train.seed, args.train.enable_full_determinism) + helper.enable_high_precision_for_bf16() + if args.train.local_rank == 0: + helper.enable_third_party_logging() + + if args.train.global_rank == 0: + save_args(args, args.train.checkpoint.output_dir) + + set_checkpoint_debug_enabled(args.train.gradient_checkpointing.debug) + + Checkpointer = build_checkpointer( + dist_backend=args.train.accelerator.fsdp_config.fsdp_mode, + ckpt_manager=args.train.checkpoint.manager, + ) + + init_parallel_state( + dp_size=args.train.accelerator.dp_size, + dp_replicate_size=args.train.accelerator.dp_replicate_size, + dp_shard_size=args.train.accelerator.dp_shard_size, + tp_size=args.train.accelerator.tp_size, + pp_size=args.train.accelerator.pp_size, + cp_size=args.train.accelerator.cp_size, + extra_parallel_sizes=args.train.accelerator.extra_parallel_sizes, + extra_parallel_placement_innermost=args.train.accelerator.extra_parallel_placement_innermost, + extra_parallel_names=args.train.accelerator.extra_parallel_names, + ulysses_size=args.train.accelerator.ulysses_size, + dp_mode=args.train.accelerator.fsdp_config.fsdp_mode, + ) + + logger.info_rank0("Prepare data") + tokenizer = build_tokenizer(args.model.tokenizer_path) + transform = _build_transform(args, tokenizer) + train_dataset = _build_train_dataset(args, transform) + dataset_length = None if not hasattr(train_dataset, "__len__") else len(train_dataset) + if args.data.datasets_type in ("mapping", "local") and dataset_length is not None: + dataset_length = dataset_length / args.train.accelerator.dp_size + args.compute_train_steps(dataset_length) + + train_dataloader = build_dataloader( + dataloader_type=args.data.dataloader.type, + dataset=train_dataset, + micro_batch_size=args.train.micro_batch_size, + global_batch_size=args.train.global_batch_size, + dataloader_batch_size=args.train.dataloader_batch_size, + max_seq_len=args.data.max_seq_len, + train_steps=args.train_steps, + dyn_bsz=args.train.dyn_bsz, + dyn_bsz_runtime=args.train.dyn_bsz_runtime, + dyn_bsz_count_mode=args.train.dyn_bsz_count_mode, + dyn_bsz_physical_overflow_ratio=args.train.dyn_bsz_physical_overflow_ratio, + dyn_bsz_buffer_size=args.data.dyn_bsz_buffer_size, + bsz_warmup_ratio=args.train.bsz_warmup_ratio, + bsz_warmup_init_mbtoken=args.train.bsz_warmup_init_mbtoken, + num_workers=args.data.dataloader.num_workers, + worker_num_threads=args.data.dataloader.worker_num_threads, + drop_last=args.data.dataloader.drop_last, + pin_memory=args.data.dataloader.pin_memory, + prefetch_factor=args.data.dataloader.prefetch_factor, + seed=args.train.seed, + collate_fn_kwargs={"pad_to_length": args.train.pad_to_length}, + save_steps=args.train.checkpoint.save_steps, + ) + + logger.info_rank0("Prepare model") + model = build_foundation_model( + config_path=args.model.config_path, + weights_path=args.model.model_path, + torch_dtype="float32" if args.train.accelerator.fsdp_config.mixed_precision.enable else "bfloat16", + init_device=args.train.init_device, + ops_implementation=args.model.ops_implementation, + ) + model_config = model.config + helper.print_device_mem_info("VRAM usage after building model") + + get_optimizer_pre_hook = getattr(model, "get_optimizer_pre_hook", None) + basic_modules = list(set(getattr(model, "_no_split_modules", None) or []) | set(args.model.basic_modules)) + model = build_parallelize_model( + model, + init_device=args.train.init_device, + weights_path=args.model.model_path, + enable_reshard_after_forward=args.train.accelerator.fsdp_config.reshard_after_forward, + mixed_precision=args.train.accelerator.fsdp_config.mixed_precision, + enable_gradient_checkpointing=args.train.gradient_checkpointing.enable, + basic_modules=basic_modules, + enable_reentrant=args.train.gradient_checkpointing.enable_reentrant, + enable_forward_prefetch=args.train.accelerator.fsdp_config.forward_prefetch, + ) + + optimizer = build_optimizer( + model, + lr=args.train.optimizer.lr, + betas=(args.train.beta1, args.train.beta2), + weight_decay=args.train.optimizer.weight_decay, + fused=True, + optimizer_type=args.train.optimizer.type, + no_decay_modules=args.train.optimizer.no_decay_modules, + no_decay_params=args.train.optimizer.no_decay_params, + ) + if get_optimizer_pre_hook is not None: + optimizer_pre_hook = get_optimizer_pre_hook(model, model_config, args.train.accelerator.fsdp_config.fsdp_mode) + optimizer.register_step_pre_hook(optimizer_pre_hook) + + lr_scheduler = build_lr_scheduler( + optimizer, + train_steps=args.train_steps * args.train.num_train_epochs, + lr=args.train.optimizer.lr, + lr_min=args.train.optimizer.lr_min, + lr_decay_style=args.train.optimizer.lr_decay_style, + lr_decay_ratio=args.train.optimizer.lr_decay_ratio, + lr_warmup_ratio=args.train.optimizer.lr_warmup_ratio, + lr_start=args.train.optimizer.lr_start, + ) + + model_assets = None + if args.train.global_rank == 0: + if args.train.wandb.enable: + wandb.init( + project=args.train.wandb.project, + name=args.train.wandb.name, + id=args.train.wandb.id, + resume="allow" if args.train.wandb.id else None, + settings=wandb.Settings(console="off"), + config={**vars(args.model), **vars(args.data), **vars(args.train)}, + ) + + model_assets = [model_config, tokenizer] + save_model_assets(args.train.checkpoint.model_assets_dir, model_assets) + + if args.train.profile.this_rank: + profiler = helper.create_profiler( + start_step=args.train.profile.start_step, + end_step=args.train.profile.end_step, + trace_dir=args.train.profile.trace_dir, + record_shapes=args.train.profile.record_shapes, + profile_memory=args.train.profile.profile_memory, + with_stack=args.train.profile.with_stack, + with_modules=args.train.profile.with_modules, + global_rank=args.train.global_rank, + ) + profiler.start() + + start_epoch, start_step, global_step = 0, 0, 0 + save_checkpoint_path = None + environ_meter = helper.EnvironMeter( + config=model_config, + global_batch_size=args.train.global_batch_size, + empty_cache_steps=args.train.empty_cache_steps, + enable_multisource=args.data.enable_multisource, + dataloader=train_dataloader, + data_path=args.data.train_path, + ) + + if args.train.checkpoint.load_path: + state = {"model": model, "optimizer": optimizer, "extra_state": {}} + Checkpointer.load(args.train.checkpoint.load_path, state) + global_step = state["extra_state"]["global_step"] + start_epoch = global_step // args.train_steps + start_step = global_step % args.train_steps + lr_scheduler.load_state_dict(state["extra_state"]["lr_scheduler"]) + train_dataloader.load_state_dict(state["extra_state"]["train_dataloader"]) + environ_meter.load_state_dict(state["extra_state"]["environ_meter"]) + torch.set_rng_state(state["extra_state"]["torch_rng_state"]) + if start_step == 0: + iter(train_dataloader) + + dist.barrier() + logger.info_rank0(f"Load distributed checkpoint from {args.train.checkpoint.load_path} successfully!") + + block_diffusion_attn_mask = _build_block_diffusion_mask(args) + helper.empty_cache() + model_fwd_context, model_bwd_context = build_activation_offloading_context( + args.train.accelerator.offload_config.enable_activation, + args.train.gradient_checkpointing.enable, + args.train.accelerator.offload_config.activation_gpu_limit, + ) + model.train() + logger.info( + f"rank{args.train.local_rank} Start training, train_steps: {args.train_steps}, " + f"epochs: {args.train.num_train_epochs}" + ) + for epoch in range(start_epoch, args.train.num_train_epochs): + if hasattr(train_dataloader, "set_epoch"): + train_dataloader.set_epoch(epoch) + + data_loader_tqdm = trange( + args.train_steps, + desc=f"Epoch {epoch + 1}/{args.train.num_train_epochs}", + total=args.train_steps, + initial=start_step, + disable=args.train.local_rank != 0, + ) + data_iterator = iter(train_dataloader) + for _ in range(start_step, args.train_steps): + global_step += 1 + + try: + micro_batches: List[Dict[str, Any]] = next(data_iterator) + except StopIteration: + logger.info(f"epoch:{epoch} Dataloader finished with drop_last {args.data.dataloader.drop_last}") + break + + if global_step == 1: + helper.print_example(example=micro_batches[0], rank=args.train.local_rank) + + total_loss = 0.0 + total_consistency_loss = 0.0 + total_confidence_loss = 0.0 + synchronize() + start_time = time.time() + num_micro_steps = len(micro_batches) + + for micro_step, micro_batch in enumerate(micro_batches): + if ( + args.train.accelerator.fsdp_config.fsdp_mode == "fsdp2" + and not args.train.accelerator.fsdp_config.reshard_after_backward + and num_micro_steps > 1 + ): + if micro_step == 0: + model.set_reshard_after_backward(False) + elif micro_step == num_micro_steps - 1: + model.set_reshard_after_backward(True) + + environ_meter.add(micro_batch) + if args.data.enable_multisource: + micro_batch.pop("ds_idx", None) + micro_batch.pop("cur_token_num", None) + micro_batch.pop("source_name", None) + + micro_batch, noisy_seq_len = _prepare_micro_batch(args, micro_batch, block_diffusion_attn_mask) + labels = micro_batch.pop("labels", None) + + with model_fwd_context: + logits = model(**micro_batch, use_cache=False, output_router_logits=False).logits + noisy_logits = logits[:, :noisy_seq_len].contiguous() if args.train.block_diffusion_mode else logits + loss, consistency_loss, confidence_loss = _compute_llada2_loss( + args, + noisy_logits=noisy_logits, + labels=labels, + num_micro_steps=num_micro_steps, + ) + + with model_bwd_context: + loss.backward() + + total_loss += loss.item() + total_consistency_loss += consistency_loss.item() / num_micro_steps + total_confidence_loss += confidence_loss.item() / num_micro_steps + del micro_batch + + grad_norm = veomni_clip_grad_norm(model, args.train.optimizer.max_grad_norm) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + if args.train.confidence_beta > 0: + total_loss, total_consistency_loss, total_confidence_loss, grad_norm = all_reduce( + (total_loss, total_consistency_loss, total_confidence_loss, grad_norm), + group=get_parallel_state().fsdp_group, + ) + else: + total_loss, grad_norm = all_reduce((total_loss, grad_norm), group=get_parallel_state().fsdp_group) + synchronize() + + delta_time = time.time() - start_time + lr = max(lr_scheduler.get_last_lr()) + train_metrics = environ_meter.step(delta_time, global_step=global_step) + + postfix = f"loss: {total_loss:.4f}, grad_norm: {grad_norm:.4f}, lr: {lr:.2e}" + if args.train.confidence_beta > 0: + postfix = ( + f"loss: {total_loss:.4f}, cons: {total_consistency_loss:.4f}, " + f"conf: {total_confidence_loss:.4f}, grad_norm: {grad_norm:.4f}, lr: {lr:.2e}" + ) + data_loader_tqdm.set_postfix_str(postfix, refresh=False) + data_loader_tqdm.update() + + if args.train.global_rank == 0 and args.train.wandb.enable: + train_metrics.update( + { + "training/loss": total_loss, + "training/grad_norm": grad_norm, + "training/lr": lr, + } + ) + if args.train.confidence_beta > 0: + train_metrics.update( + { + "training/cons_loss": total_consistency_loss, + "training/conf_loss": total_confidence_loss, + } + ) + wandb.log(train_metrics, step=global_step) + + if args.train.profile.this_rank and global_step <= args.train.profile.end_step: + profiler.step() + if global_step == args.train.profile.end_step: + profiler.stop() + + if args.train.checkpoint.save_steps and global_step % args.train.checkpoint.save_steps == 0: + helper.empty_cache() + save_checkpoint_path = os.path.join(args.train.checkpoint.save_path, f"global_step_{global_step}") + state = { + "model": model, + "optimizer": optimizer, + "extra_state": { + "global_step": global_step, + "lr_scheduler": lr_scheduler.state_dict(), + "train_dataloader": train_dataloader.state_dict(), + "environ_meter": environ_meter.state_dict(), + "torch_rng_state": torch.get_rng_state(), + }, + } + Checkpointer.save(args.train.checkpoint.save_path, state, global_steps=global_step) + + dist.barrier() + logger.info_rank0(f"Distributed checkpoint saved at {save_checkpoint_path} successfully!") + + data_loader_tqdm.close() + start_step = 0 + helper.print_device_mem_info(f"VRAM usage after epoch {epoch + 1}") + if args.train.checkpoint.save_epochs and (epoch + 1) % args.train.checkpoint.save_epochs == 0: + helper.empty_cache() + save_checkpoint_path = os.path.join(args.train.checkpoint.save_path, f"global_step_{global_step}") + state = { + "model": model, + "optimizer": optimizer, + "extra_state": { + "global_step": global_step, + "lr_scheduler": lr_scheduler.state_dict(), + "train_dataloader": train_dataloader.state_dict(), + "environ_meter": environ_meter.state_dict(), + "torch_rng_state": torch.get_rng_state(), + }, + } + Checkpointer.save(args.train.checkpoint.save_path, state, global_steps=global_step) + dist.barrier() + logger.info_rank0(f"Distributed checkpoint saved at {save_checkpoint_path} successfully!") + + synchronize() + del optimizer, lr_scheduler + helper.empty_cache() + if args.train.checkpoint.save_hf_weights and save_checkpoint_path is not None: + hf_weights_path = os.path.join(save_checkpoint_path, "hf_ckpt") + save_hf_safetensor( + save_hf_safetensor_path=hf_weights_path, + ckpt_manager=args.train.checkpoint.manager, + model_assets=model_assets, + save_checkpoint_path=save_checkpoint_path, + is_rank_0=args.train.global_rank == 0, + model=model, + fqn_to_index_mapping=args.model.fqn_to_index_mapping, + ) + + dist.barrier() + dist.destroy_process_group() diff --git a/train.sh b/train.sh index 7c567b1..bc6fc9e 100644 --- a/train.sh +++ b/train.sh @@ -6,7 +6,16 @@ export TOKENIZERS_PARALLELISM=false export TORCH_NCCL_AVOID_RECORD_STREAMS=1 NNODES=${NNODES:=1} -NPROC_PER_NODE=${NPROC_PER_NODE:=$(nvidia-smi --list-gpus | wc -l)} +if [[ -z "${NPROC_PER_NODE:-}" ]]; then + if command -v nvidia-smi >/dev/null 2>&1; then + NPROC_PER_NODE=$(nvidia-smi --list-gpus | wc -l | tr -d ' ') + elif command -v npu-smi >/dev/null 2>&1; then + NPROC_PER_NODE=$(npu-smi info 2>/dev/null | awk '/^[[:space:]]*[0-9]+[[:space:]]+[0-9]+/ {print $1}' | sort -u | wc -l | tr -d ' ') + [[ "$NPROC_PER_NODE" == "0" ]] && NPROC_PER_NODE=1 + else + NPROC_PER_NODE=1 + fi +fi NODE_RANK=${NODE_RANK:=0} MASTER_ADDR=${MASTER_ADDR:=0.0.0.0} MASTER_PORT=${MASTER_PORT:=12345} From 15e9f2005308951f7003faafcc02392120fdfe52 Mon Sep 17 00:00:00 2001 From: shaojiemike Date: Thu, 25 Jun 2026 17:47:25 +0800 Subject: [PATCH 02/11] Fix LLaDA2 EP parallel plan for latest VeOmni --- models/llada2_moe/parallel_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/llada2_moe/parallel_plan.py b/models/llada2_moe/parallel_plan.py index fd31fd3..391467a 100644 --- a/models/llada2_moe/parallel_plan.py +++ b/models/llada2_moe/parallel_plan.py @@ -10,6 +10,6 @@ def get_parallel_plan(): "model.layers.*.mlp.experts.down_proj": Shard(0), } parallel_plan = ParallelPlan( - ep_plan=ep_plan, + extra_parallel_plan={"ep": ep_plan}, ) return parallel_plan From 5648a7475bdf0e5837742a9d95854deca5b921cb Mon Sep 17 00:00:00 2001 From: Akij Date: Thu, 23 Jul 2026 23:37:28 +0800 Subject: [PATCH 03/11] docs: add MIGRATION_NOTES with Phase 0 baseline and Phase 1 upstream-sync findings Phase 0 (inventory): environment reality (Win11/py3.14, no torch/veomni, RTX 5070 Ti), git state, VeOmni pin (600fe6d, 495 commits behind main), architecture map, and the model-identity branch points Phase 4 depends on. Phase 1 (upstream sync): fork is 1 ahead / 0 behind upstream/main; no upstream branch/tag adds LLaDA2.1/2.2. Records upstream open PR #22 (veomni-npu-migration) as the approved basis for Phase 2. --- MIGRATION_NOTES.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 MIGRATION_NOTES.md diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md new file mode 100644 index 0000000..3f07a45 --- /dev/null +++ b/MIGRATION_NOTES.md @@ -0,0 +1,94 @@ +# dFactory Modernization — Migration Notes + +Running log of what changed, why, what is verified, and what is not. +Legend: ✅ verified in this environment · 🟡 statically checked only (could not run) · ❌ broken/blocked · ⏭️ not attempted. + +--- + +## Environment (this machine) + +| Thing | Value | Consequence | +| --- | --- | --- | +| OS | Windows 11 Pro (26200) | VeOmni's distributed stack (FSDP2/EP/torchrun) is Linux-first; `flash-attn` has no Windows wheels | +| Python | 3.14.5 (system, `C:\Python314`, no venv) | Very new; many ML wheels lag | +| torch | **not installed** (`pip install torch` dry-run resolves to torch 2.13.0 cp314 win) | Can install CPU torch for isolated verification | +| transformers / accelerate / flash-attn / veomni | **not installed** | Repo cannot be imported as-is | +| GPU | NVIDIA RTX 5070 Ti Laptop (Blackwell sm_120, 12 GB) | Real GPU present, but Blackwell needs CUDA 12.8+/recent torch; 12 GB can't hold 16B/100B models | +| huggingface_hub | 1.21.0 (installed) | ✅ Can download config files for Phase 4a | +| Network | GitHub + HF reachable | ✅ | + +**Verification reality:** Anything that needs VeOmni or multi-GPU cannot be executed here. VeOmni-independent +logic (MoE merge/split, block-diffusion mask, config diff/validation, RoPE config patching) *can* be unit-tested +with CPU torch and will be. Everything else is static-only and labelled 🟡. + +--- + +## Baseline (Phase 0) — read-only inventory + +### Repository identity & git state +- Fork remote (`origin`): `https://github.com/Akicou/dFactory.git` +- Upstream: `https://github.com/inclusionAI/dFactory` (added as `upstream` during inventory) +- Fork `main` = **1 commit ahead, 0 behind** `upstream/main`. + - The single ahead commit is `1d5545d` (2026-03-27) *"feat: add RoPE/YaRN context extension to 64k/128k/256k with Nemotron streaming dataset"* — the fork's only local divergence. +- Last **real** upstream code change: `8c8133b` (2025-12-04) *"fix[dfactory] Refactor micro_batch tensor device handling"*. Everything newer on `upstream/main` up to `92b6890` (2025-12-15) is Dependabot GitHub-Actions bumps. +- Upstream branches checked for LLaDA2.1/2.2 support: `add_tpd`, `fix/train_llada2_bd`, `utdawn-patch-1` are all fully merged into main (0 branch-only commits). `jlzhou/noise` has 3 branch-only commits but they only refactor `sft_noise_transition` (make maskable-mask optional, rename vars, reformat) — **no** 2.1/2.2 model support anywhere upstream. No upstream tags exist. + - **Conclusion: upstream does NOT already provide LLaDA2.1/2.2 support. Phase 4 remains necessary.** + +### VeOmni submodule +- Declared in `.gitmodules`: path `VeOmni`, url `https://github.com/ByteDance-Seed/VeOmni.git`. +- Pinned commit: `600fe6d7442392fd3ddefad4b6c8d3c0002fed1c` (2025-10-17, *"[core] fix: use flash_attention_2 backend (#124)"*). +- **The submodule is NOT checked out** — the `VeOmni/` directory is empty. `git submodule update --init` is required before anything runs. +- VeOmni `main` HEAD at inventory time: `1f58648` (2026-07-22). The pin is **495 commits behind** main (~9 months). + +### Dependency expectations vs installed +- No root `requirements.txt` / `setup.py` / `pyproject.toml`. Install path is `pip install -e VeOmni/` (or `uv sync --extra gpu` inside `VeOmni/`) — i.e. dFactory inherits VeOmni's dependency set. Only `docs/requirements.txt` exists at repo level (Sphinx). +- LLaDA2 checkpoint `config.json` files declare `"transformers_version": "4.52.3"` — this is the transformers reference the bundled `trust_remote_code` modeling files were authored against (relevant ceiling for Phase 3). +- Installed here: none of torch/transformers/accelerate/flash-attn/veomni. So the repo currently does **not** import in this environment (expected). + +### Architecture map + +**Training entrypoints** (`tasks/`): +- `train_llada2_bd.py` (573 L) — main SFT loop. Objectives: **block-diffusion** (`block_diffusion_mode: true`, builds a custom block-diffusion attention mask via `block_diffusion_mask()`, concatenates `[noisy_input_ids, clean_input_ids]`) and **full-attention random-mask** (`block_diffusion_mode: false`, `attention_mask=None`). Loss = cross-entropy over masked positions, with `same_token_labels` toggling next-token shift vs no-shift. +- `train_llada2_bd_with_dparallel.py` (622 L) — variant adding **trainable parallel decoding** (extra block-diffusion handling around the forward pass). +- `train_longctx.py` (550 L) — fork's long-context entrypoint. Adds `data_type="text"` → `process_mdm_text_example` and `datasets_type="nemotron_streaming"` → `NemotronStreamingDataset`; wires `context_parallel_size` (CP). + +**Diffusion objective / masking**: +- `block_diffusion_mask()` in `train_llada2_bd.py:122` — composes M_BD (block diagonal), M_OBC (offset block causal), M_BC (block causal) over a length-`2·max_seq_len` grid. +- `tasks/dataset/data_transform.py` — `sft_noise_transition()` samples a masking ratio σ∈`noise_range` and flips maskable tokens to `mask_token_id` (**hardcoded 156895** in transforms and in `train_llada2_bd.py:208/216`). Transforms: `process_mdm_sft_example` (chat), `process_mdm_tokenized_example` (pre-tokenised), `process_mdm_text_example` (raw pretraining text, all positions maskable — fork addition). + +**Data pipeline** (`tasks/dataset/`): +- `dataset.py` → `build_local_dataset` (HF `load_from_disk` + VeOmni `MappingDataset`). +- `nemotron_dataset.py` → `NemotronStreamingDataset` (streaming `nvidia/Nemotron-Pretraining-Specialized-v1.1`, char-length prefilter; fork addition). +- VeOmni provides `build_dataloader`, `build_iterative_dataset`, `build_mapping_dataset`. + +**MoE merge/split conversion**: +- `scripts/moe_convertor.py` — `moe_merge()` stacks per-expert `{gate,up,down}_proj` into `[num_experts, …]` tensors; `split_moe_experts()` reverses it. Iterates layers `first_k_dense_replace..num_hidden_layers`. Uses VeOmni `build_tokenizer` + `save_model_weights`. + +**Model definition** (`models/llada2_moe/`): +- `configuration_llada2_moe.py` — `LLaDA2MoeConfig`, `model_type = "llada2_moe_veomni"` (note: the copies bundled under `configs/model_configs/*/` use `model_type = "llada2_moe"`). +- `modeling_llada2_moe.py` (1581 L) — full HF-style model. Attn backends: eager/sdpa/flex_attention. RoPE via `LLaDA2MoeRotaryEmbedding` using `ROPE_INIT_FUNCTIONS[rope_type]` (so `rope_scaling` types linear/dynamic/yarn/longrope are honoured by transformers' shared init), partial rotary (`partial_rotary_factor=0.5`, `rotary_dim=64`). MoE via `LLaDA2MoeExperts` (fused param tensors) + `LLaDA2MoeSparseMoeBlock`. +- `parallel_plan.py` — expert-parallel sharding plan (`Shard(0)` on merged expert tensors). + +### Places the code branches on model identity / architecture (Phase 4 dependency list) +1. `modeling_llada2_moe.py:340` & `:395` — `if self.config.model_type == "llada2_moe_veomni"` selects fused-MoE experts + `_fuse_moe_forward` (uses VeOmni `fused_moe_forward`) vs the reference `nn.ModuleList` experts + `_forward`. **The two model_type strings (`llada2_moe` vs `llada2_moe_veomni`) are load-bearing.** +2. `moe_convertor.py:35-42` — reads `config.num_hidden_layers`, `config.num_experts`, `config.first_k_dense_replace` to drive merge; `split_moe_experts` reads `config.num_experts`. No validation that architecture is actually LLaDA2 / that fields are self-consistent. +3. `configuration_llada2_moe.py` defaults are generic/small (vocab 30592, 16 experts) and are **overridden** by each `config.json` (real mini: 20 layers, hidden 2048, 256 experts, 8/tok, 1 shared, vocab 157184). Extra `config.json` keys not in the class (`norm_topk_prob`, `use_rmsnorm`, `router_dtype`, `score_function`, `moe_router_enable_expert_bias`, `rotary_dim`, `using_split_qkv_in_self_attention`, …) are absorbed via `**kwargs`. +4. `train_llada2_bd.py:208/216` & `data_transform.py` — `mask_token_id=156895` hardcoded (assumes LLaDA2.0 tokenizer). Must be re-checked per checkpoint in Phase 4a. +5. `configs/model_configs/{llada2_mini,llada2_flash}/config.json` — pinned `num_experts=256`, `num_experts_per_tok=8`, `num_shared_experts=1`, `n_group=8`, `topk_group=4`, `max_position_embeddings=16384`. These are the 2.0-era assumptions Phase 4 must revisit against 2.1/2.2. + +### CI +- `.github/workflows/`: `build-docs.yml`, `deploy-docs.yml` only. **No test/CI pipeline** — Phase 5 has nothing to wire into yet. + +--- + +## Phase log + +### Phase 1 — Sync with upstream ✅ (verified via git) +- `upstream` remote added (`https://github.com/inclusionAI/dFactory`). Fork `main` is **1 ahead / 0 behind** — nothing to merge or rebase to catch up. The one local commit (`1d5545d`, long-context) is preserved as-is. +- Upstream branches/tags: no tag exists; `add_tpd`/`fix/train_llada2_bd`/`utdawn-patch-1` already merged; `jlzhou/noise` only refactors the noise fn. **No upstream branch/tag adds LLaDA2.1/2.2 support.** +- **Upstream OPEN PR #22 `veomni-npu-migration` ("[codex] Migrate dFactory to latest VeOmni with NPU support")** — directly addresses Phase 2: + - Bumps VeOmni `600fe6d` → `8ca09d7` (2026-06-24); migrates model registration, training args, dataloader/checkpoint/optimizer usage, EP plan (`ParallelPlan(extra_parallel_plan={...})`), and SFT configs to current VeOmni APIs; collapses the two entrypoints into `tasks/train_llada2_common.py`; adds Ascend NPU support. + - **Validated on Ascend 910B2**: tiny-model + real-weight (`LLaDA2.0-mini-preview`, 7 shards) parity vs. legacy stack — loss/logits/full-gradient diff = `0.0`, 0 missing/unexpected keys. (Validated on NPU, *not* on this machine's GPU.) + - Does **not** add 2.1/2.2 support → Phase 4 still required. Does **not** touch the fork's long-context files. Merges onto the fork with a single trivial conflict in `tasks/dataset/__init__.py`. + - **Decision (user-approved): adopt PR #22 as the Phase 2 basis**, preserving the fork's long-context work and porting `train_longctx.py` to the new VeOmni API by following PR #22's pattern. +- Branching: work proceeds on chained `modernize/phaseN-*` branches (each off the prior), atomic commits, so pieces remain independently reviewable. From 614761943a5294bf447bff1985792e56d4556e2a Mon Sep 17 00:00:00 2001 From: Akij Date: Fri, 24 Jul 2026 00:09:58 +0800 Subject: [PATCH 04/11] Port train_longctx + longctx configs to new VeOmni API (Phase 2) PR #22 migrated the SFT entrypoints but not the fork's long-context files. This ports them to VeOmni 8ca09d7 without duplicating the training loop: - train_llada2_common.py: add backwards-compatible transform_builder / dataset_builder hooks to run_llada2_training so other entrypoints can reuse the validated loop. - train_longctx.py: rewritten from a ~550-line copy of the old flat-API loop into a thin extension using those hooks (text transform + Nemotron streaming). block_diffusion_mode=false reproduces the prior non-BD behaviour exactly. - configs/longctx/{64k,128k,256k}: migrated flat -> nested schema; field names (cp_size, offload_config.enable_activation, optimizer.lr_min, checkpoint.save_steps) verified against veomni/arguments/arguments_types.py. Verified: py_compile on all changed .py; all YAML parses; VeOmni symbols used by moe_convertor.py / dataset.py confirmed still exported at 8ca09d7. NOT runtime-verified: VeOmni not importable on Windows/py3.14. train_longctx.py and configs/longctx are my ports, NOT part of PR #22's NPU validation. --- MIGRATION_NOTES.md | 29 + configs/longctx/llada2_mini_longctx_128k.yaml | 88 +-- configs/longctx/llada2_mini_longctx_256k.yaml | 88 +-- configs/longctx/llada2_mini_longctx_64k.yaml | 106 ++-- tasks/train_llada2_common.py | 17 +- tasks/train_longctx.py | 545 ++---------------- 6 files changed, 255 insertions(+), 618 deletions(-) diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md index 3f07a45..578401c 100644 --- a/MIGRATION_NOTES.md +++ b/MIGRATION_NOTES.md @@ -92,3 +92,32 @@ with CPU torch and will be. Everything else is static-only and labelled 🟡. - Does **not** add 2.1/2.2 support → Phase 4 still required. Does **not** touch the fork's long-context files. Merges onto the fork with a single trivial conflict in `tasks/dataset/__init__.py`. - **Decision (user-approved): adopt PR #22 as the Phase 2 basis**, preserving the fork's long-context work and porting `train_longctx.py` to the new VeOmni API by following PR #22's pattern. - Branching: work proceeds on chained `modernize/phaseN-*` branches (each off the prior), atomic commits, so pieces remain independently reviewable. + +### Phase 2 — VeOmni submodule bump 🟡 (adopted from a validated upstream PR; not runtime-verified here) +- **VeOmni bumped `600fe6d` (2025-10) → `8ca09d7` (2026-06-24)** by merging upstream PR #22. Submodule initialised locally (blobless partial clone) at `8ca09d7`. +- Merge conflict (only one): `tasks/dataset/__init__.py` — resolved by keeping the fork's nemotron exports. +- **VeOmni API changes that dFactory's surface had to adapt to** (audited against the checked-out `8ca09d7` source; most were handled by PR #22, the long-context port I did by hand): + + | Area | Old (600fe6d) | New (8ca09d7) | + | --- | --- | --- | + | Arg entrypoint | `veomni.utils.arguments`; flat `Arguments` | `veomni.arguments`; `VeOmniArguments` base; nested config groups | + | Parallel/FSDP args | `train.data_parallel_mode`, `train.tensor_parallel_size`, `enable_full_shard`, `enable_mixed_precision`, `enable_fsdp_offload` | `train.accelerator.{tp_size,ep_size,pp_size,cp_size,ulysses_size}`, `train.accelerator.fsdp_config.{fsdp_mode,offload,reshard_after_*,forward_prefetch,mixed_precision.enable}`, `train.accelerator.offload_config.{enable_activation,activation_gpu_limit}` | + | Optimizer args | `train.lr`, `train.optimizer`, `train.weight_decay`, `train.max_grad_norm` | `train.optimizer.{type,lr,lr_min,lr_warmup_ratio,lr_decay_style,lr_decay_ratio,weight_decay,max_grad_norm}` | + | Checkpoint args | `train.output_dir`, `train.ckpt_manager`, `train.load_checkpoint_path`, `train.save_steps` | `train.checkpoint.{output_dir,manager,load_path,save_steps,save_epochs,save_hf_weights}` | + | Grad-ckpt / wandb | `train.enable_gradient_checkpointing`, `train.use_wandb` | `train.gradient_checkpointing.enable`, `train.wandb.enable` | + | Model ops | `build_foundation_model(attn_implementation=, moe_implementation=, force_use_huggingface=)` | `build_foundation_model(ops_implementation=…)`; `model.ops_implementation.{attn,moe,…}_implementation`. `moe_implementation: fused` → `fused_triton` (GPU) | + | Model registration | `ModelRegistry.register_modeling_path("models.llada2_moe")` | `import models.llada2_moe` (self-registers via `MODEL_CONFIG_REGISTRY`/`MODELING_REGISTRY` in its `__init__.py`) | + | EP plan | `ParallelPlan(ep_plan={…})` | `ParallelPlan(extra_parallel_plan={"ep": …})` | + | Dataset build | `build_iterative_dataset` / `build_mapping_dataset` | `build_dataset(dataset_name, **kwargs)` (registry); `build_dataloader(dataloader_type=…, …)`; custom iterables still accepted as `dataset=` | + | Grad clipping | `model.clip_grad_norm_(…)` | `veomni_clip_grad_norm(model, max_grad_norm)` | + | Dist backend | `get_nccl_backend()` | `get_dist_comm_backend()` / `is_nccl_backend()` | + | HF export | `ckpt_to_state_dict(…)` + `save_model_weights(…)` | `save_hf_safetensor(…)` | + | `compute_train_steps` | `args.train.compute_train_steps(max_seq_len, train_size, dataset_length)` | `args.compute_train_steps(dataset_length)`; step count at `args.train_steps` | + +- **Still valid, unchanged:** `veomni.models.{build_tokenizer,save_model_weights,save_model_assets,build_foundation_model}` (so `scripts/moe_convertor.py` needs no change); `veomni.data.dataset.{IterativeDataset,MappingDataset}` (so `tasks/dataset/dataset.py` needs no change). +- **My hand-ported changes on top of PR #22:** + - `tasks/train_llada2_common.py`: added two optional, backwards-compatible hooks to `run_llada2_training(arguments_cls, transform_builder=None, dataset_builder=None)` so alternative entrypoints can reuse the exact validated loop. + - `tasks/train_longctx.py`: rewritten from a ~550-line copy of the old flat-API loop into a thin extension that reuses `run_llada2_training` via those hooks (text transform + Nemotron streaming). Behaviour preserved: long-context uses `block_diffusion_mode: false`, which reproduces the old non-block-diffusion path exactly (plus optional `confidence_beta`, default 0 = off). + - `configs/longctx/*.yaml` (64k/128k/256k): migrated from the flat schema to the new nested schema; `cp_size`/`offload_config.enable_activation`/`lr_min`/`save_steps` field names verified against `veomni/arguments/arguments_types.py`. +- **Verification:** all changed `.py` pass `py_compile`; all 7 YAMLs parse. VeOmni arg field names cross-checked against the `8ca09d7` source. **Not runtime-verified** — VeOmni is not importable here (Windows/py3.14; needs Linux + CUDA/NPU + flash-attn). PR #22 itself was hardware-validated upstream on Ascend 910B2 (parity = 0.0), but `train_longctx.py` and `configs/longctx/*` are **my** ports and were **not** part of that validation. +- **Deliverable (Phase 2):** highest VeOmni commit reached = `8ca09d7`. The Phase 5 smoke test could not be used as the per-bump gate in this environment (no runnable VeOmni); this is the blocker, documented here and in the commit. diff --git a/configs/longctx/llada2_mini_longctx_128k.yaml b/configs/longctx/llada2_mini_longctx_128k.yaml index df747e5..d7b84a6 100644 --- a/configs/longctx/llada2_mini_longctx_128k.yaml +++ b/configs/longctx/llada2_mini_longctx_128k.yaml @@ -1,20 +1,19 @@ # Context-extension fine-tune: LLaDA2-mini 64k -> 128k (YaRN 4x) # -# Pre-requisite: produce the 64k checkpoint first, then: -# +# New (post-PR#22) nested VeOmni schema. Prereq: produce the 64k checkpoint first, then: # python scripts/extend_rope_context.py \ # --model_path ./configs/model_configs/llada2_mini_64k \ # --output_path ./configs/model_configs/llada2_mini_128k \ # --target_length 131072 --method yarn -# # Set model_path below to the 64k fine-tuned weights, not the base weights. model: config_path: ./configs/model_configs/llada2_mini_128k - model_path: ./output/llada2_mini_longctx_64k/hf_ckpt # 64k fine-tuned weights + model_path: ./output/llada2_mini_longctx_64k/hf_ckpt # 64k fine-tuned weights tokenizer_path: ./LLaDA2.0-mini - attn_implementation: sdpa - moe_implementation: fused + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton data: data_type: text @@ -31,50 +30,57 @@ data: shuffle_buffer: 5000 noise_range_low: 0.3 noise_range_high: 0.8 - num_workers: 4 - dataloader_type: native - drop_last: true + dataloader: + type: native + num_workers: 4 + drop_last: true + pin_memory: true train: - output_dir: ./output/llada2_mini_longctx_128k - data_parallel_mode: fsdp2 - tensor_parallel_size: 1 - ulysses_parallel_size: 1 - expert_parallel_size: 1 - context_parallel_size: 2 # 128k sequences need CP>=2 on most GPU configs + dyn_bsz: false global_batch_size: 4 micro_batch_size: 1 num_train_epochs: 1 - rmpad: false - rmpad_with_pos_ids: false bsz_warmup_ratio: 0.0 - dyn_bsz_margin: 0 - dyn_bsz_buffer_size: 200 - optimizer: adamw - beta1: 0.9 - beta2: 0.999 - lr: 1.0e-5 - lr_min: 1.0e-6 - lr_warmup_ratio: 0.05 - lr_decay_style: cosine - lr_decay_ratio: 1.0 - weight_decay: 0.1 - max_grad_norm: 1.0 - enable_mixed_precision: true - enable_gradient_checkpointing: true - enable_full_shard: true - enable_fsdp_offload: false - enable_activation_offload: false init_device: meta broadcast_model_weights_from_rank0: true enable_full_determinism: false empty_cache_steps: 100 - ckpt_manager: dcp - load_checkpoint_path: "" - save_steps: 200 - save_epochs: 1 - save_hf_weights: true + beta1: 0.9 + beta2: 0.999 block_diffusion_mode: false same_token_labels: false - use_wandb: false - log_steps: 1 + optimizer: + type: adamw + lr: 1.0e-5 + lr_min: 1.0e-6 + lr_warmup_ratio: 0.05 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 2 # 128k sequences need CP>=2 on most GPU configs + fsdp_config: + fsdp_mode: fsdp2 + offload: false + mixed_precision: + enable: true + offload_config: + enable_activation: false + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./output/llada2_mini_longctx_128k + manager: dcp + load_path: null + save_steps: 200 + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/longctx/llada2_mini_longctx_256k.yaml b/configs/longctx/llada2_mini_longctx_256k.yaml index 79790b0..30a8828 100644 --- a/configs/longctx/llada2_mini_longctx_256k.yaml +++ b/configs/longctx/llada2_mini_longctx_256k.yaml @@ -1,21 +1,20 @@ # Context-extension fine-tune: LLaDA2-mini 128k -> 256k (LongRoPE 8x) # -# Pre-requisite: produce the 128k checkpoint first, then: -# +# New (post-PR#22) nested VeOmni schema. Prereq: produce the 128k checkpoint first, then: # python scripts/extend_rope_context.py \ # --model_path ./configs/model_configs/llada2_mini_128k \ # --output_path ./configs/model_configs/llada2_mini_256k \ # --target_length 262144 --method longrope -# # Set model_path below to the 128k fine-tuned weights. # LongRoPE bootstraps long_factor/short_factor=1.0; fine-tuning learns them. model: config_path: ./configs/model_configs/llada2_mini_256k - model_path: ./output/llada2_mini_longctx_128k/hf_ckpt # 128k fine-tuned weights + model_path: ./output/llada2_mini_longctx_128k/hf_ckpt # 128k fine-tuned weights tokenizer_path: ./LLaDA2.0-mini - attn_implementation: sdpa - moe_implementation: fused + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton data: data_type: text @@ -32,50 +31,57 @@ data: shuffle_buffer: 2000 noise_range_low: 0.3 noise_range_high: 0.8 - num_workers: 4 - dataloader_type: native - drop_last: true + dataloader: + type: native + num_workers: 4 + drop_last: true + pin_memory: true train: - output_dir: ./output/llada2_mini_longctx_256k - data_parallel_mode: fsdp2 - tensor_parallel_size: 1 - ulysses_parallel_size: 1 - expert_parallel_size: 1 - context_parallel_size: 4 # 256k requires CP>=4; increase if OOM + dyn_bsz: false global_batch_size: 2 micro_batch_size: 1 num_train_epochs: 1 - rmpad: false - rmpad_with_pos_ids: false bsz_warmup_ratio: 0.0 - dyn_bsz_margin: 0 - dyn_bsz_buffer_size: 200 - optimizer: adamw - beta1: 0.9 - beta2: 0.999 - lr: 5.0e-6 - lr_min: 5.0e-7 - lr_warmup_ratio: 0.05 - lr_decay_style: cosine - lr_decay_ratio: 1.0 - weight_decay: 0.1 - max_grad_norm: 1.0 - enable_mixed_precision: true - enable_gradient_checkpointing: true - enable_full_shard: true - enable_fsdp_offload: false - enable_activation_offload: true # needed for 256k activations init_device: meta broadcast_model_weights_from_rank0: true enable_full_determinism: false empty_cache_steps: 50 - ckpt_manager: dcp - load_checkpoint_path: "" - save_steps: 100 - save_epochs: 1 - save_hf_weights: true + beta1: 0.9 + beta2: 0.999 block_diffusion_mode: false same_token_labels: false - use_wandb: false - log_steps: 1 + optimizer: + type: adamw + lr: 5.0e-6 + lr_min: 5.0e-7 + lr_warmup_ratio: 0.05 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 4 # 256k requires CP>=4; increase if OOM + fsdp_config: + fsdp_mode: fsdp2 + offload: false + mixed_precision: + enable: true + offload_config: + enable_activation: true # needed for 256k activations + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./output/llada2_mini_longctx_256k + manager: dcp + load_path: null + save_steps: 100 + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/longctx/llada2_mini_longctx_64k.yaml b/configs/longctx/llada2_mini_longctx_64k.yaml index cea2f89..4abde70 100644 --- a/configs/longctx/llada2_mini_longctx_64k.yaml +++ b/configs/longctx/llada2_mini_longctx_64k.yaml @@ -1,84 +1,90 @@ # Context-extension fine-tune: LLaDA2-mini 16k -> 64k (YaRN 2x) # -# Pre-requisite: run extend_rope_context.py first to produce the patched config dir: -# +# New (post-PR#22) nested VeOmni schema. Prereq: patch the RoPE config first: # python scripts/extend_rope_context.py \ # --model_path ./configs/model_configs/llada2_mini \ # --output_path ./configs/model_configs/llada2_mini_64k \ # --target_length 65536 --method yarn -# # Then launch: -# bash train.sh tasks/train_longctx.py --config configs/longctx/llada2_mini_longctx_64k.yaml +# PYTHONPATH=$(pwd)/VeOmni:$(pwd)/tasks:$PYTHONPATH \ +# sh train.sh tasks/train_longctx.py --config configs/longctx/llada2_mini_longctx_64k.yaml model: - config_path: ./configs/model_configs/llada2_mini_64k # patched by extend_rope_context.py - model_path: ./LLaDA2.0-mini # base weights (HF checkpoint dir) + config_path: ./configs/model_configs/llada2_mini_64k # patched by extend_rope_context.py + model_path: ./LLaDA2.0-mini # base weights (HF checkpoint dir) tokenizer_path: ./LLaDA2.0-mini - attn_implementation: sdpa # use flash_attention_2 if installed - moe_implementation: fused + ops_implementation: + attn_implementation: sdpa # flash_attention_2 if installed + moe_implementation: fused_triton data: - # Nemotron dataset settings - data_type: text # triggers process_mdm_text_example - datasets_type: nemotron_streaming # triggers build_nemotron_streaming_dataset - nemotron_subsets: # which subsets to include (all 5 by default) + data_type: text # -> process_mdm_text_example + datasets_type: nemotron_streaming # -> build_nemotron_streaming_dataset + nemotron_subsets: - Nemotron-Pretraining-Code-Concepts - Nemotron-Pretraining-Economics - Nemotron-Pretraining-Formal-Logic - Nemotron-Pretraining-Multiple-Choice - Nemotron-Pretraining-Unconditional-Algorithmic max_seq_len: 65536 - # Only keep examples whose estimated token count is in [0.5 * max_seq_len, max_seq_len]. - # This ensures gradient signal comes from positions beyond the original 16k limit. + # Keep only examples whose estimated token count is in [0.5*max_seq_len, max_seq_len] + # so gradient signal comes from positions beyond the original 16k limit. min_token_len: 32768 # 0.5 * 65536 max_token_len: 65536 shuffle_buffer: 10000 noise_range_low: 0.3 noise_range_high: 0.8 - num_workers: 4 - dataloader_type: native - drop_last: true + dataloader: + type: native + num_workers: 4 + drop_last: true + pin_memory: true train: - output_dir: ./output/llada2_mini_longctx_64k - data_parallel_mode: fsdp2 - tensor_parallel_size: 1 - ulysses_parallel_size: 1 - expert_parallel_size: 1 - context_parallel_size: 1 # increase to 2 if 65536 OOMs + dyn_bsz: false global_batch_size: 8 micro_batch_size: 1 num_train_epochs: 1 - rmpad: false - rmpad_with_pos_ids: false bsz_warmup_ratio: 0.0 - dyn_bsz_margin: 0 - dyn_bsz_buffer_size: 200 - optimizer: adamw - beta1: 0.9 - beta2: 0.999 - lr: 2.0e-5 - lr_min: 2.0e-6 - lr_warmup_ratio: 0.05 - lr_decay_style: cosine - lr_decay_ratio: 1.0 - weight_decay: 0.1 - max_grad_norm: 1.0 - enable_mixed_precision: true - enable_gradient_checkpointing: true - enable_full_shard: true - enable_fsdp_offload: false - enable_activation_offload: false init_device: meta broadcast_model_weights_from_rank0: true enable_full_determinism: false empty_cache_steps: 100 - ckpt_manager: dcp - load_checkpoint_path: "" - save_steps: 200 - save_epochs: 1 - save_hf_weights: true - block_diffusion_mode: false # full attention for context-extension phase + beta1: 0.9 + beta2: 0.999 + block_diffusion_mode: false # full attention for the context-extension phase same_token_labels: false - use_wandb: false - log_steps: 1 + optimizer: + type: adamw + lr: 2.0e-5 + lr_min: 2.0e-6 + lr_warmup_ratio: 0.05 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 # increase to 2 if 65536 OOMs + fsdp_config: + fsdp_mode: fsdp2 + offload: false + mixed_precision: + enable: true + offload_config: + enable_activation: false + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./output/llada2_mini_longctx_64k + manager: dcp + load_path: null + save_steps: 200 + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/tasks/train_llada2_common.py b/tasks/train_llada2_common.py index a9618af..f79936a 100644 --- a/tasks/train_llada2_common.py +++ b/tasks/train_llada2_common.py @@ -293,7 +293,18 @@ def _compute_llada2_loss( return combined_loss / num_micro_steps, consistency_loss, confidence_loss -def run_llada2_training(arguments_cls=LLaDA2Arguments): +def run_llada2_training(arguments_cls=LLaDA2Arguments, transform_builder=None, dataset_builder=None): + """Shared LLaDA2 training loop. + + ``transform_builder(args, tokenizer)`` and ``dataset_builder(args, transform)`` are + optional extension hooks (default to the SFT/tokenid builders below) so that + entrypoints such as ``train_longctx`` can plug in a different sample transform or + dataset source while reusing the exact, validated training loop. + """ + if transform_builder is None: + transform_builder = _build_transform + if dataset_builder is None: + dataset_builder = _build_train_dataset nccl_timeout = os.getenv("NCCL_TIMEOUT", None) pg_nccl_timeout = None if nccl_timeout is not None and is_nccl_backend(): @@ -336,8 +347,8 @@ def run_llada2_training(arguments_cls=LLaDA2Arguments): logger.info_rank0("Prepare data") tokenizer = build_tokenizer(args.model.tokenizer_path) - transform = _build_transform(args, tokenizer) - train_dataset = _build_train_dataset(args, transform) + transform = transform_builder(args, tokenizer) + train_dataset = dataset_builder(args, transform) dataset_length = None if not hasattr(train_dataset, "__len__") else len(train_dataset) if args.data.datasets_type in ("mapping", "local") and dataset_length is not None: dataset_length = dataset_length / args.train.accelerator.dp_size diff --git a/tasks/train_longctx.py b/tasks/train_longctx.py index 82e0484..2bcac9f 100644 --- a/tasks/train_longctx.py +++ b/tasks/train_longctx.py @@ -1,178 +1,111 @@ """ -Long-context fine-tuning script for LLaDA2 using Nemotron-Pretraining-Specialized-v1.1. +Long-context fine-tuning entrypoint for LLaDA2 (Nemotron-Pretraining-Specialized-v1.1). -Extends train_llada2_bd.py with two additions: - 1. data_type="text" -> uses process_mdm_text_example (raw pretraining text) - 2. datasets_type="nemotron_streaming" -> uses NemotronStreamingDataset +This is a thin extension of the shared ``train_llada2_common.run_llada2_training`` loop +(the validated VeOmni path), adding two things on top of the SFT entrypoint: -All other training logic (FSDP2, MoE, diffusion loss, checkpointing) is unchanged. + 1. ``data_type="text"`` -> ``process_mdm_text_example`` (raw pretraining text, + every position maskable — no prompt prefix). + 2. ``datasets_type="nemotron_streaming"`` -> ``NemotronStreamingDataset`` (streamed, no + full download). + +Everything else (FSDP2, MoE dispatch, diffusion loss, checkpointing, HF export) is inherited +unchanged from ``run_llada2_training`` via its ``transform_builder`` / ``dataset_builder`` hooks, +so this file carries no copy of the training loop. Long-context runs use full attention +(``block_diffusion_mode: false``) and typically ``cp_size > 1``. + +Ported from the pre-PR#22 flat VeOmni API to the current nested API (VeOmni 8ca09d7). +NOTE: not runtime-verified in this environment (VeOmni is not installable on Windows/py3.14); +it mirrors the validated SFT path structurally and passes py_compile. See MIGRATION_NOTES.md. Launch: - bash train.sh tasks/train_longctx.py \\ - --config configs/longctx/llada2_mini_longctx_64k.yaml + PYTHONPATH=$(pwd)/VeOmni:$(pwd)/tasks:$PYTHONPATH \\ + sh train.sh tasks/train_longctx.py --config configs/longctx/llada2_mini_longctx_64k.yaml """ -import json -import os -import time -from dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field from functools import partial -from typing import Any, Dict, List, Literal, Optional, Tuple - -import torch -import torch.distributed as dist -import wandb -from tqdm import trange +from typing import List, Literal, Optional -from veomni.checkpoint import build_checkpointer, ckpt_to_state_dict -from veomni.data import build_dataloader, build_iterative_dataset, build_mapping_dataset -from veomni.distributed.offloading import build_activation_offloading_context -from veomni.distributed.parallel_state import get_parallel_state, init_parallel_state -from veomni.distributed.torch_parallelize import build_parallelize_model -from veomni.models import build_foundation_model, build_tokenizer, save_model_assets, save_model_weights -from veomni.optim import build_lr_scheduler, build_optimizer -from veomni.utils import helper -from veomni.utils.arguments import DataArguments, ModelArguments, TrainingArguments, parse_args, save_args -from veomni.utils.device import get_device_type, get_nccl_backend, get_torch_device, synchronize -from veomni.utils.dist_utils import all_reduce -from veomni.models.registry import ModelRegistry +import models.llada2_moe # noqa: F401 - registers LLaDA2 MoE with the VeOmni loader. -ModelRegistry.register_modeling_path("models.llada2_moe") - -from dataset.data_transform import ( - process_mdm_sft_example, - process_mdm_text_example, - process_mdm_tokenized_example, +from train_llada2_common import ( + LLaDA2Arguments, + LLaDA2DataArguments, + _build_train_dataset, + _build_transform, + run_llada2_training, ) -from dataset import build_local_dataset, build_nemotron_streaming_dataset, NEMOTRON_SUBSETS - -logger = helper.create_logger(__name__) +try: + from dataset import NEMOTRON_SUBSETS, build_nemotron_streaming_dataset + from dataset.data_transform import process_mdm_text_example +except ImportError: # when launched with repo root (not tasks/) on PYTHONPATH + from tasks.dataset import NEMOTRON_SUBSETS, build_nemotron_streaming_dataset + from tasks.dataset.data_transform import process_mdm_text_example -# --------------------------------------------------------------------------- -# Argument dataclasses -# --------------------------------------------------------------------------- @dataclass -class LLaDA2ModelArguments(ModelArguments): - attn_implementation: Optional[Literal["eager", "sdpa", "flex_attention"]] = field( - default="sdpa", - metadata={"help": "Attention implementation to use."}, - ) - - -@dataclass -class LLaDA2DataArguments(DataArguments): +class LongCtxDataArguments(LLaDA2DataArguments): + # Widen the parent Literals to add the long-context / streaming options. data_type: Literal["conversation", "tokenid", "text"] = field( default="text", - metadata={"help": "Type of training data: 'text' for Nemotron raw text."}, + metadata={"help": "'text' for raw Nemotron pretraining text (all positions maskable)."}, ) - datasets_type: Literal["mapping", "local", "nemotron_streaming"] = field( + datasets_type: Literal["mapping", "iterable", "local", "nemotron_streaming"] = field( default="nemotron_streaming", - metadata={"help": "Dataset backend type."}, - ) - text_keys: str = field( - default="text", - metadata={"help": "Column name to read text from."}, + metadata={"help": "Dataset backend; 'nemotron_streaming' streams from the Hub."}, ) - noise_range_low: float = field(default=0.3, metadata={"help": "Min mask ratio."}) - noise_range_high: float = field(default=0.8, metadata={"help": "Max mask ratio."}) - # Nemotron-specific + # Nemotron-streaming specific knobs. nemotron_subsets: Optional[List[str]] = field( default=None, - metadata={"help": "Nemotron subset names to use. None = all 5 subsets."}, + metadata={"help": "Nemotron subset names. None = all supported subsets."}, ) min_token_len: int = field( default=0, - metadata={"help": "Minimum estimated token length for Nemotron filter (0=disabled)."}, + metadata={"help": "Lower bound (estimated tokens) for the streaming length filter; 0=off."}, ) max_token_len: int = field( default=0, - metadata={"help": "Maximum estimated token length for Nemotron filter (0=disabled)."}, + metadata={"help": "Upper bound (estimated tokens) for the streaming length filter; 0=off."}, ) shuffle_buffer: int = field( default=10_000, - metadata={"help": "Shuffle buffer size for streaming dataset."}, + metadata={"help": "Shuffle-buffer size for the streaming dataset."}, ) def __post_init__(self) -> None: + # Parent defaults text_keys to messages/input_ids; for raw text it should be "text". + if self.text_keys is None and self.data_type == "text": + self.text_keys = "text" super().__post_init__() - if self.noise_range_low > self.noise_range_high: - raise ValueError( - f"noise_range_low ({self.noise_range_low}) > noise_range_high ({self.noise_range_high})" - ) if self.nemotron_subsets is not None: unknown = set(self.nemotron_subsets) - set(NEMOTRON_SUBSETS) if unknown: - raise ValueError(f"Unknown Nemotron subsets: {unknown}. Valid: {NEMOTRON_SUBSETS}") + raise ValueError(f"Unknown Nemotron subsets: {sorted(unknown)}. Valid: {NEMOTRON_SUBSETS}") @dataclass -class LLaDA2TrainingArguments(TrainingArguments): - beta1: float = field(default=0.9, metadata={"help": "AdamW beta1."}) - beta2: float = field(default=0.999, metadata={"help": "AdamW beta2."}) - block_diffusion_mode: bool = field( - default=False, - metadata={"help": "Use block-diffusion attention mask. Keep False for context extension."}, - ) - block_size: int = field(default=32, metadata={"help": "Block size for block diffusion."}) - same_token_labels: bool = field( - default=False, - metadata={"help": "No-shift labels (True) vs next-token shift (False)."}, - ) - - -@dataclass -class Arguments: - model: LLaDA2ModelArguments = field(default_factory=LLaDA2ModelArguments) - data: LLaDA2DataArguments = field(default_factory=LLaDA2DataArguments) - train: LLaDA2TrainingArguments = field(default_factory=LLaDA2TrainingArguments) +class LongCtxArguments(LLaDA2Arguments): + data: LongCtxDataArguments = field(default_factory=LongCtxDataArguments) -# --------------------------------------------------------------------------- -# Dataset construction -# --------------------------------------------------------------------------- - -def _build_transform(args: Arguments, tokenizer): - noise_range: Tuple[float, float] = (args.data.noise_range_low, args.data.noise_range_high) - mask_token_id = 156895 - +def _build_longctx_transform(args: LongCtxArguments, tokenizer): + """Text -> masked-diffusion transform; otherwise delegate to the shared SFT builder.""" if args.data.data_type == "text": return partial( process_mdm_text_example, tokenizer=tokenizer, max_seq_len=args.data.max_seq_len, text_keys=args.data.text_keys, - noise_range=noise_range, - mask_token_id=mask_token_id, + noise_range=(args.data.noise_range_low, args.data.noise_range_high), + mask_token_id=args.data.mask_token_id, ) - elif args.data.data_type == "conversation": - if not tokenizer.chat_template: - raise ValueError("No chat template found in the tokenizer.") - return partial( - process_mdm_sft_example, - tokenizer=tokenizer, - max_seq_len=args.data.max_seq_len, - text_keys=args.data.text_keys, - noise_range=noise_range, - mask_token_id=mask_token_id, - ) - elif args.data.data_type == "tokenid": - return partial( - process_mdm_tokenized_example, - max_seq_len=args.data.max_seq_len, - text_keys=args.data.text_keys, - noise_range=noise_range, - mask_token_id=mask_token_id, - ) - else: - raise NotImplementedError(f"Unsupported data_type: {args.data.data_type!r}") - + return _build_transform(args, tokenizer) -def _build_train_dataset(args: Arguments, transform): - dt = args.data.datasets_type - if dt == "nemotron_streaming": - logger.info_rank0("Building NemotronStreamingDataset (streaming, no full download)") +def _build_longctx_dataset(args: LongCtxArguments, transform): + """Nemotron streaming source; otherwise delegate to the shared dataset builder.""" + if args.data.datasets_type == "nemotron_streaming": return build_nemotron_streaming_dataset( subsets=args.data.nemotron_subsets, min_token_len=args.data.min_token_len, @@ -181,370 +114,16 @@ def _build_train_dataset(args: Arguments, transform): shuffle_buffer=args.data.shuffle_buffer, seed=args.train.seed, ) - elif dt == "mapping": - logger.info_rank0("Building mapping dataset") - return build_mapping_dataset(args.data.train_path, transform=transform) - elif dt == "local": - logger.info_rank0("Building local dataset") - return build_local_dataset(args.data.train_path, transform=transform) - elif dt == "iterable": - logger.info_rank0("Building iterative dataset") - return build_iterative_dataset(args.data.train_path, transform=transform, seed=args.train.seed) - else: - raise NotImplementedError(f"Unsupported datasets_type: {dt!r}") - + return _build_train_dataset(args, transform) -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- def main() -> None: - dist.init_process_group(backend=get_nccl_backend()) - args = parse_args(Arguments) - - logger.info(f"Process rank: {args.train.global_rank}, world size: {args.train.world_size}") - logger.info_rank0(json.dumps(asdict(args), indent=2)) - get_torch_device().set_device(f"{get_device_type()}:{args.train.local_rank}") - helper.set_seed(args.train.seed, args.train.enable_full_determinism) - if args.train.local_rank == 0: - helper.enable_third_party_logging() - if args.train.global_rank == 0: - save_args(args, args.train.output_dir) - - Checkpointer = build_checkpointer( - dist_backend=args.train.data_parallel_mode, - ckpt_manager=args.train.ckpt_manager, - ) - - init_parallel_state( - dp_size=args.train.data_parallel_size, - dp_replicate_size=args.train.data_parallel_replicate_size, - dp_shard_size=args.train.data_parallel_shard_size, - tp_size=args.train.tensor_parallel_size, - ep_size=args.train.expert_parallel_size, - pp_size=args.train.pipeline_parallel_size, - cp_size=args.train.context_parallel_size, - ulysses_size=args.train.ulysses_parallel_size, - dp_mode=args.train.data_parallel_mode, - ) - - logger.info_rank0("Prepare data") - tokenizer = build_tokenizer(args.model.tokenizer_path) - transform = _build_transform(args, tokenizer) - train_dataset = _build_train_dataset(args, transform) - - dataset_length = None if not hasattr(train_dataset, "__len__") else len(train_dataset) - if args.data.datasets_type in ("mapping", "local") and dataset_length is not None: - dataset_length = dataset_length / args.train.data_parallel_size - args.train.compute_train_steps(args.data.max_seq_len, args.data.train_size, dataset_length) - - train_dataloader = build_dataloader( - dataset=train_dataset, - micro_batch_size=args.train.micro_batch_size, - global_batch_size=args.train.global_batch_size, - dataloader_batch_size=args.train.dataloader_batch_size, - seed=args.train.seed, - max_seq_len=args.data.max_seq_len, - train_steps=args.train.train_steps, - rmpad=args.train.rmpad, - rmpad_with_pos_ids=args.train.rmpad_with_pos_ids, - bsz_warmup_ratio=args.train.bsz_warmup_ratio, - bsz_warmup_init_mbtoken=args.train.bsz_warmup_init_mbtoken, - dyn_bsz_margin=args.train.dyn_bsz_margin, - dyn_bsz_buffer_size=args.train.dyn_bsz_buffer_size, - num_workers=args.data.num_workers, - drop_last=args.data.drop_last, - pin_memory=args.data.pin_memory, - prefetch_factor=args.data.prefetch_factor, - ) - - logger.info_rank0("Prepare model") - model = build_foundation_model( - config_path=args.model.config_path, - weights_path=args.model.model_path, - torch_dtype="float32" if args.train.enable_mixed_precision else "bfloat16", - attn_implementation=args.model.attn_implementation, - moe_implementation=args.model.moe_implementation, - init_device=args.train.init_device, - force_use_huggingface=args.model.force_use_huggingface, - ) - model_config = model.config - helper.print_device_mem_info("VRAM after model build") - - get_optimizer_pre_hook = getattr(model, "get_optimizer_pre_hook", None) - model = build_parallelize_model( - model, - init_device=args.train.init_device, - weights_path=args.model.model_path, - enable_full_shard=args.train.enable_full_shard, - enable_mixed_precision=args.train.enable_mixed_precision, - enable_gradient_checkpointing=args.train.enable_gradient_checkpointing, - enable_fsdp_offload=args.train.enable_fsdp_offload, - basic_modules=model._no_split_modules + args.model.basic_modules, - enable_reentrant=args.train.enable_reentrant, - enable_forward_prefetch=args.train.enable_forward_prefetch, - broadcast_model_weights_from_rank0=args.train.broadcast_model_weights_from_rank0, + run_llada2_training( + LongCtxArguments, + transform_builder=_build_longctx_transform, + dataset_builder=_build_longctx_dataset, ) - optimizer = build_optimizer( - model, - lr=args.train.lr, - betas=(args.train.beta1, args.train.beta2), - weight_decay=args.train.weight_decay, - fused=True, - optimizer_type=args.train.optimizer, - ) - if get_optimizer_pre_hook is not None: - optimizer_pre_hook = get_optimizer_pre_hook(model, model_config, args.train.data_parallel_mode) - optimizer.register_step_pre_hook(optimizer_pre_hook) - - lr_scheduler = build_lr_scheduler( - optimizer, - train_steps=args.train.train_steps * args.train.num_train_epochs, - lr=args.train.lr, - lr_min=args.train.lr_min, - lr_decay_style=args.train.lr_decay_style, - lr_decay_ratio=args.train.lr_decay_ratio, - lr_warmup_ratio=args.train.lr_warmup_ratio, - lr_start=args.train.lr_start, - ) - - if args.train.global_rank == 0: - if args.train.use_wandb: - wandb.init( - project=args.train.wandb_project, - name=args.train.wandb_name, - config={**vars(args.model), **vars(args.data), **vars(args.train)}, - ) - model_assets = [model_config, tokenizer] - save_model_assets(args.train.model_assets_dir, model_assets) - - if args.train.profile_this_rank: - profiler = helper.create_profiler( - start_step=args.train.profile_start_step, - end_step=args.train.profile_end_step, - trace_dir=args.train.profile_trace_dir, - record_shapes=args.train.profile_record_shapes, - profile_memory=args.train.profile_profile_memory, - with_stack=args.train.profile_with_stack, - global_rank=args.train.global_rank, - ) - profiler.start() - - start_epoch, start_step, global_step = 0, 0, 0 - save_checkpoint_path = None - environ_meter = helper.EnvironMeter( - config=model_config, - global_batch_size=args.train.global_batch_size, - rmpad=args.train.rmpad, - rmpad_with_pos_ids=args.train.rmpad_with_pos_ids, - empty_cache_steps=args.train.empty_cache_steps, - enable_multisource=args.data.enable_multisource, - dataloader=train_dataloader, - data_path=args.data.train_path, - ) - - if args.train.load_checkpoint_path: - state: Dict[str, Any] = {"model": model, "optimizer": optimizer, "extra_state": {}} - Checkpointer.load(args.train.load_checkpoint_path, state) - global_step = state["extra_state"]["global_step"] - start_epoch = global_step // args.train.train_steps - start_step = global_step % args.train.train_steps - lr_scheduler.load_state_dict(state["extra_state"]["lr_scheduler"]) - train_dataloader.load_state_dict(state["extra_state"]["train_dataloader"]) - environ_meter.load_state_dict(state["extra_state"]["environ_meter"]) - torch.set_rng_state(state["extra_state"]["torch_rng_state"]) - if start_step == 0: - iter(train_dataloader) - dist.barrier() - logger.info_rank0(f"Loaded checkpoint from {args.train.load_checkpoint_path}") - - helper.empty_cache() - model_fwd_context, model_bwd_context = build_activation_offloading_context( - args.train.enable_activation_offload, - args.train.enable_gradient_checkpointing, - args.train.activation_gpu_limit, - ) - model.train() - logger.info( - f"rank{args.train.local_rank} Start training, " - f"train_steps={args.train.train_steps}, epochs={args.train.num_train_epochs}" - ) - - for epoch in range(start_epoch, args.train.num_train_epochs): - if hasattr(train_dataloader, "set_epoch"): - train_dataloader.set_epoch(epoch) - - data_loader_tqdm = trange( - args.train.train_steps, - desc=f"Epoch {epoch + 1}/{args.train.num_train_epochs}", - total=args.train.train_steps, - initial=start_step, - disable=args.train.local_rank != 0, - ) - data_iterator = iter(train_dataloader) - - for _ in range(start_step, args.train.train_steps): - global_step += 1 - try: - micro_batches: List[Dict[str, Any]] = next(data_iterator) - except StopIteration: - logger.info(f"epoch:{epoch} Dataloader finished (drop_last={args.data.drop_last})") - break - - if global_step == 1: - helper.print_example(example=micro_batches[0], rank=args.train.local_rank) - - total_loss = 0.0 - synchronize() - start_time = time.time() - - for micro_batch in micro_batches: - environ_meter.add(micro_batch) - if args.data.enable_multisource: - micro_batch.pop("ds_idx", None) - micro_batch.pop("source_name", None) - - # Long-context training always uses full attention (no block diffusion) - micro_batch["attention_mask"] = None - - micro_batch = { - k: v.to(get_device_type(), non_blocking=True) if isinstance(v, torch.Tensor) else v - for k, v in micro_batch.items() - } - - labels = micro_batch.pop("labels", None) - # noisy_input_ids is the diffusion input; input_ids is kept as x0 reference - micro_batch.pop("noisy_input_ids", None) - - with model_fwd_context: - logits: torch.Tensor = model( - **micro_batch, use_cache=False, output_router_logits=False - ).logits - - if args.train.same_token_labels: - unscaled_loss = torch.nn.functional.cross_entropy( - logits.view(-1, logits.shape[-1]), - labels.view(-1), - reduction="none", - ) - loss = unscaled_loss.sum() / (labels != -100).sum() / len(micro_batches) - else: - shifted_logits = logits[:, :-1, :].contiguous() - shifted_labels = labels[:, 1:].contiguous() - unscaled_loss = torch.nn.functional.cross_entropy( - shifted_logits.view(-1, shifted_logits.shape[-1]), - shifted_labels.view(-1), - reduction="none", - ).view(shifted_logits.shape[0], -1) - loss = unscaled_loss.sum() / (shifted_labels != -100).sum() / len(micro_batches) - - with model_bwd_context: - loss.backward() - - total_loss += loss.item() - del micro_batch - - if hasattr(model, "clip_grad_norm_"): - _gn = model.clip_grad_norm_(args.train.max_grad_norm) - grad_norm = _gn.item() if hasattr(_gn, "item") else float(_gn) - else: - grad_norm = float( - torch.nn.utils.clip_grad_norm_(model.parameters(), args.train.max_grad_norm) - ) - - optimizer.step() - lr_scheduler.step() - optimizer.zero_grad() - - if hasattr(grad_norm, "full_tensor"): - grad_norm = grad_norm.full_tensor().item() - - total_loss, grad_norm = all_reduce( - (total_loss, grad_norm), group=get_parallel_state().fsdp_group - ) - synchronize() - delta_time = time.time() - start_time - lr = max(lr_scheduler.get_last_lr()) - train_metrics = environ_meter.step(delta_time, global_step=global_step) - - data_loader_tqdm.set_postfix_str( - f"loss: {total_loss:.3f}, grad_norm: {grad_norm:.3f}, lr: {lr:.2e}" - ) - data_loader_tqdm.update() - - if args.train.global_rank == 0 and args.train.use_wandb: - train_metrics.update( - {"training/loss": total_loss, "training/grad_norm": grad_norm, "training/lr": lr} - ) - wandb.log(train_metrics, step=global_step) - - if args.train.profile_this_rank and global_step <= args.train.profile_end_step: - profiler.step() - if global_step == args.train.profile_end_step: - profiler.stop() - - if args.train.save_steps and global_step % args.train.save_steps == 0: - helper.empty_cache() - save_checkpoint_path = os.path.join( - args.train.save_checkpoint_path, f"global_step_{global_step}" - ) - state = { - "model": model, - "optimizer": optimizer, - "extra_state": { - "global_step": global_step, - "lr_scheduler": lr_scheduler.state_dict(), - "train_dataloader": train_dataloader.state_dict(), - "environ_meter": environ_meter.state_dict(), - "torch_rng_state": torch.get_rng_state(), - }, - } - Checkpointer.save(args.train.save_checkpoint_path, state, global_steps=global_step) - dist.barrier() - logger.info_rank0(f"Checkpoint saved at {save_checkpoint_path}") - - data_loader_tqdm.close() - start_step = 0 - helper.print_device_mem_info(f"VRAM after epoch {epoch + 1}") - - if args.train.save_epochs and (epoch + 1) % args.train.save_epochs == 0: - helper.empty_cache() - save_checkpoint_path = os.path.join( - args.train.save_checkpoint_path, f"global_step_{global_step}" - ) - state = { - "model": model, - "optimizer": optimizer, - "extra_state": { - "global_step": global_step, - "lr_scheduler": lr_scheduler.state_dict(), - "train_dataloader": train_dataloader.state_dict(), - "environ_meter": environ_meter.state_dict(), - "torch_rng_state": torch.get_rng_state(), - }, - } - Checkpointer.save(args.train.save_checkpoint_path, state, global_steps=global_step) - dist.barrier() - logger.info_rank0(f"Epoch checkpoint saved at {save_checkpoint_path}") - - synchronize() - del optimizer, lr_scheduler - helper.empty_cache() - - if args.train.global_rank == 0 and args.train.save_hf_weights and save_checkpoint_path is not None: - hf_weights_path = os.path.join(save_checkpoint_path, "hf_ckpt") - model_state_dict = ckpt_to_state_dict( - save_checkpoint_path=save_checkpoint_path, - output_dir=args.train.output_dir, - ckpt_manager=args.train.ckpt_manager, - ) - save_model_weights(hf_weights_path, model_state_dict, model_assets=model_assets) - logger.info_rank0(f"HuggingFace weights saved at {hf_weights_path}") - - dist.barrier() - dist.destroy_process_group() - if __name__ == "__main__": main() From be1d60f55e4ffb9ba44bb4dc5c38b64c42b00b10 Mon Sep 17 00:00:00 2001 From: Akij Date: Fri, 24 Jul 2026 00:18:17 +0800 Subject: [PATCH 05/11] Add pinned requirements aligned to VeOmni 8ca09d7 (Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transcribes VeOmni 8ca09d7's authoritative dependency constraints into a root requirements.txt for the pip install path (uv --extra gpu remains recommended): - Python >=3.11,<3.13 (this box's 3.14 is out of range — documented blocker). - torch 2.11.0+cu130, transformers 5.9.0, datasets<=2.21.0, tiktoken, einops, etc. - accelerate intentionally absent (VeOmni is FSDP2-native; nothing imports it). Findings recorded in MIGRATION_NOTES: - transformers 4.52.3 ceiling is inference-side (bundled checkpoint code); training uses the repo's own modeling file, import-validated under 5.9.0 by PR #22. - torch FSDP / device-mesh deprecation surface is inside VeOmni (already current), not dFactory's own code; parallel_plan._tensor import matches VeOmni convention. Verified: extend_rope_context.py --help runs (pure stdlib). Everything torch/veomni dependent is NOT runtime-verifiable here; declared explicitly. --- MIGRATION_NOTES.md | 15 +++++++++++++ requirements.txt | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 requirements.txt diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md index 578401c..4ebfa20 100644 --- a/MIGRATION_NOTES.md +++ b/MIGRATION_NOTES.md @@ -121,3 +121,18 @@ with CPU torch and will be. Everything else is static-only and labelled 🟡. - `configs/longctx/*.yaml` (64k/128k/256k): migrated from the flat schema to the new nested schema; `cp_size`/`offload_config.enable_activation`/`lr_min`/`save_steps` field names verified against `veomni/arguments/arguments_types.py`. - **Verification:** all changed `.py` pass `py_compile`; all 7 YAMLs parse. VeOmni arg field names cross-checked against the `8ca09d7` source. **Not runtime-verified** — VeOmni is not importable here (Windows/py3.14; needs Linux + CUDA/NPU + flash-attn). PR #22 itself was hardware-validated upstream on Ascend 910B2 (parity = 0.0), but `train_longctx.py` and `configs/longctx/*` are **my** ports and were **not** part of that validation. - **Deliverable (Phase 2):** highest VeOmni commit reached = `8ca09d7`. The Phase 5 smoke test could not be used as the per-bump gate in this environment (no runnable VeOmni); this is the blocker, documented here and in the commit. + +### Phase 3 — Dependencies & environment 🟡 (spec transcribed from VeOmni; install NOT verifiable here) +- Authoritative source of truth = VeOmni `8ca09d7`'s `pyproject.toml`. Transcribed into a root `requirements.txt` (pip path) alongside the recommended `uv sync --extra gpu` path. +- **Pins (per VeOmni 8ca09d7):** + - **Python `>=3.11,<3.13`** — VeOmni's `requires-python`. **This box runs Python 3.14 → out of range.** Hard blocker for running anything here; documented, not "fixed" (would require a 3.12 interpreter). + - **torch `2.11.0+cu130`** (torchvision 0.26.0, torchaudio 2.11.0), CUDA 13.0 wheel index. (VeOmni also defines a CPU extra `torch==2.7.1+cpu` and an NPU extra `torch-npu==2.7.1`.) + - **transformers `5.9.0`** (VeOmni's `transformers-stable` default group; pip users are told to `pip install transformers==5.9.0`). + - datasets `>=2.20.0,<=2.21.0`, torchdata `>=0.8.0,<1.0`, tiktoken `>=0.9.0`, einops `>=0.8.1`, blobfile, wandb, matplotlib, psutil, safetensors, huggingface_hub. + - Optional GPU kernels: flash-attn (+ flash-attn-3/4 in VeOmni's extra), liger-kernel. +- **`accelerate` is NOT a dependency** — VeOmni is FSDP2-native; neither VeOmni 8ca09d7 nor dFactory import `accelerate`. The task brief listed it, but there is nothing to pin. (Noted rather than silently ignored.) +- **transformers ceiling / trust_remote_code:** the LLaDA2 HF checkpoints bundle `configuration_llada2_moe.py` + `modeling_llada2_moe.py` authored for `transformers 4.52.3`. But dFactory *training* does not use those bundled files — it uses the repo's own `models/llada2_moe/modeling_llada2_moe.py`, whose transformers imports were exercised by PR #22's upstream "model/config registry import passes" check under transformers 5.9.0. `scripts/moe_convertor.py` only loads the *config* (not modeling) via `trust_remote_code=True`, which is forward-compatible. → training tolerates transformers 5.9.0; the 4.52.3 ceiling is an **inference-side** concern for end-users, verified concretely in Phase 4a. +- **Deprecations:** the torch FSDP / `torch.distributed` device-mesh surface that the brief flagged lives entirely inside **VeOmni** (already current at 8ca09d7), not in dFactory's own code. dFactory's own torch usage (`dist.init_process_group`, `torch.distributed._tensor.Shard`, `torch.load(weights_only=True, mmap=True)`, `set_default_dtype`, `cross_entropy`, rng state) is not deprecated in torch 2.11. In particular `from torch.distributed._tensor import Shard` in `parallel_plan.py` matches VeOmni 8ca09d7's own convention (it imports `Shard` from `_tensor` in ~7 files) — so it is left as-is rather than "fixed" to `torch.distributed.tensor`. +- **Import / `--help` verification:** + - ✅ `scripts/extend_rope_context.py --help` runs cleanly (pure stdlib — the one entrypoint with no torch/veomni deps). + - 🟡 `moe_convertor.py`, `train_llada2_bd.py`, `train_longctx.py`, `train_llada2_common.py` `--help`/import require torch + veomni + transformers → **cannot run here**. Declared unverified. (Phase 5 refactors `moe_convertor` so its merge/split logic becomes importable & testable without veomni.) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..be1d9d8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,54 @@ +# dFactory training dependencies — aligned to the pinned VeOmni submodule (8ca09d7, 2026-06-24). +# +# STATUS: target spec, transcribed from VeOmni 8ca09d7's pyproject.toml (its core +# `dependencies` + the `transformers-stable` default group + the `gpu` extra). +# NOT install-verified in the authoring environment (Windows + Python 3.14 — which VeOmni +# does NOT support: it requires Python >=3.11,<3.13). Verify on Linux + Python 3.12 + CUDA 13. +# +# Authoritative / recommended install is VeOmni's own uv extras (resolves the +cu130 torch +# variant and the flash-attn source builds that a flat requirements file cannot express): +# cd VeOmni && uv sync --extra gpu && source .venv/bin/activate && cd .. +# pip install -e VeOmni/ # installs the `veomni` package dFactory imports +# This file is the pip-path equivalent for the GPU training stack. + +# --- Python ----------------------------------------------------------------- +# Python >=3.11,<3.13 (VeOmni `requires-python`). 3.12 recommended. + +# --- PyTorch (CUDA 13.0 build) ---------------------------------------------- +# Do NOT install these from plain PyPI; use the CUDA 13.0 wheel index: +# pip install torch==2.11.0 torchvision==0.26.0 torchaudio==2.11.0 \ +# --index-url https://download.pytorch.org/whl/cu130 +torch==2.11.0 +torchvision==0.26.0 +torchaudio==2.11.0 + +# --- Hugging Face stack ----------------------------------------------------- +transformers==5.9.0 # VeOmni `transformers-stable` pin at 8ca09d7. + # NOTE ceiling: LLaDA2 HF checkpoints bundle trust_remote_code + # modeling files authored for transformers 4.52.3. dFactory + # *training* uses its own (5.9.0-validated) modeling file, but + # end-users running a checkpoint for inference may need a + # transformers matching the bundled code — see MIGRATION_NOTES. +datasets>=2.20.0,<=2.21.0 # VeOmni core pin (upper bound is load-bearing). +safetensors>=0.4.0 +huggingface_hub>=0.34.0 +tiktoken>=0.9.0 # LLaDA2 tokenizer backend. + +# --- Distributed / training utils (VeOmni core) ----------------------------- +torchdata>=0.8.0,<1.0 +einops>=0.8.1 +blobfile>=3.0.0 +packaging>=23.0,<26.0 +psutil +wandb +setuptools +matplotlib>=3.7 +tqdm + +# --- Optional GPU kernels (see VeOmni `gpu` extra; source/prebuilt) ---------- +# flash-attn # enables attn_implementation: flash_attention_2 / flex_attention +# liger-kernel # fused RMSNorm / SwiGLU / RoPE (auto-detected if importable) + +# --- Deliberately ABSENT ---------------------------------------------------- +# accelerate: VeOmni is FSDP2-native and does not depend on HF accelerate. +# (The task brief mentioned it, but neither VeOmni 8ca09d7 nor dFactory import it.) From 2eb48f5a6e7b2d2fc37decadc42cb1b0675aa63a Mon Sep 17 00:00:00 2001 From: Akij Date: Fri, 24 Jul 2026 00:53:55 +0800 Subject: [PATCH 06/11] Extend model support to LLaDA2.1/2.2 (Phase 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4a — checkpoint loading (config diffs verified on real downloaded configs): - Diffed config.json across 2.0-mini/2.1-mini/2.1-flash/2.2-flash. Shared MoE tensor layout (256 experts, top-8, vocab 157184); 2.2 adds expert_capacity=48, block_size=32, use_qk_norm=true, mpe=131072, rope_theta=3e6. - configuration_llada2_moe.py: expert_capacity/block_size now explicit params, not **kwargs. - compat.py (new, dependency-free): validate_llada2_config() fails loudly on non-LLaDA2 / inconsistent configs and warns loudly on 2.2 block routing. Verified on all 4 real configs + negative cases. - moe_convertor.py: merge/split now importable without veomni (lazy import); validation wired in. - New model_configs/{llada2_1_mini,llada2_1_flash,llada2_2_flash} + matching configs/sft/*. 4b — long-context ceiling: documented (per-doc padding, no packing; block-diffusion mask is O((2L)^2) and unusable long — loud guard added; longctx uses full attention). Real ceiling depends on VeOmni CP + memory, unverifiable here. 4c — docs/UNIMPLEMENTED.md: Levenshtein editing (§3.1), block routing (top-48/256 per block), L-EBPO (§3.2) — grounded in paper + downloaded 2.2 modeling code. editing.py stubs raise NotImplementedError (verified), not wired into training. None implemented (unpublished, out of scope). Verified here (stdlib): validator on real+new configs, negative cases, YAML/JSON parse, py_compile, stubs raise. NOT verified: real weight load / conversion (needs torch+transformers+veomni+weights). --- .gitignore | 1 + MIGRATION_NOTES.md | 34 +++++ .../model_configs/llada2_1_flash/config.json | 56 ++++++++ .../configuration_llada2_moe.py | 88 ++++++++++++ .../model_configs/llada2_1_mini/config.json | 56 ++++++++ .../llada2_1_mini/configuration_llada2_moe.py | 88 ++++++++++++ .../model_configs/llada2_2_flash/config.json | 58 ++++++++ .../configuration_llada2_moe.py | 94 +++++++++++++ configs/sft/llada2_1_flash_bd_sft.yaml | 81 +++++++++++ configs/sft/llada2_1_mini_bd_sft.yaml | 81 +++++++++++ configs/sft/llada2_2_flash_bd_sft.yaml | 94 +++++++++++++ docs/UNIMPLEMENTED.md | 88 ++++++++++++ models/llada2_moe/compat.py | 126 ++++++++++++++++++ models/llada2_moe/configuration_llada2_moe.py | 11 ++ models/llada2_moe/editing.py | 81 +++++++++++ scripts/moe_convertor.py | 31 ++++- tasks/train_llada2_common.py | 12 ++ 17 files changed, 1075 insertions(+), 5 deletions(-) create mode 100644 configs/model_configs/llada2_1_flash/config.json create mode 100644 configs/model_configs/llada2_1_flash/configuration_llada2_moe.py create mode 100644 configs/model_configs/llada2_1_mini/config.json create mode 100644 configs/model_configs/llada2_1_mini/configuration_llada2_moe.py create mode 100644 configs/model_configs/llada2_2_flash/config.json create mode 100644 configs/model_configs/llada2_2_flash/configuration_llada2_moe.py create mode 100644 configs/sft/llada2_1_flash_bd_sft.yaml create mode 100644 configs/sft/llada2_1_mini_bd_sft.yaml create mode 100644 configs/sft/llada2_2_flash_bd_sft.yaml create mode 100644 docs/UNIMPLEMENTED.md create mode 100644 models/llada2_moe/compat.py create mode 100644 models/llada2_moe/editing.py diff --git a/.gitignore b/.gitignore index 7a47753..f7279cc 100644 --- a/.gitignore +++ b/.gitignore @@ -238,3 +238,4 @@ compile_commands.json Cargo.lock lmms-eval +.scratch_configs/ diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md index 4ebfa20..05a1dd4 100644 --- a/MIGRATION_NOTES.md +++ b/MIGRATION_NOTES.md @@ -136,3 +136,37 @@ with CPU torch and will be. Everything else is static-only and labelled 🟡. - **Import / `--help` verification:** - ✅ `scripts/extend_rope_context.py --help` runs cleanly (pure stdlib — the one entrypoint with no torch/veomni deps). - 🟡 `moe_convertor.py`, `train_llada2_bd.py`, `train_longctx.py`, `train_llada2_common.py` `--help`/import require torch + veomni + transformers → **cannot run here**. Declared unverified. (Phase 5 refactors `moe_convertor` so its merge/split logic becomes importable & testable without veomni.) + +### Phase 4 — Extend model support to 2.1 / 2.2 + +#### 4a — checkpoint loading ✅ (config diff/validation verified on real downloaded configs) +- Downloaded **config files only** (config.json, configuration_llada2_moe.py, modeling_llada2_moe.py — no weights) for `LLaDA2.0-mini`, `LLaDA2.1-mini`, `LLaDA2.1-flash`, `LLaDA2.2-flash` and diffed them. +- **All four share** `model_type: llada2_moe`, `architectures: [LLaDA2MoeModelLM]`, `num_experts: 256`, `num_experts_per_tok: 8`, `num_shared_experts: 1`, `n_group: 8`, `topk_group: 4`, `vocab_size: 157184`, `pad_token_id: 156892`. → the MoE/expert tensor layout `moe_convertor` touches is **identical across 2.0/2.1/2.2**; no vocab expansion for editing tokens (DELETE/INSERT live inside the existing 157184 vocab). +- **Fields that changed** (2.0-mini / 2.1-mini / 2.1-flash / 2.2-flash): + - `max_position_embeddings`: 32768 / 32768 / 32768 / **131072** (2.2 = 128K). + - `rope_theta`: 600000 / 600000 / 600000 / **3000000** (2.2). + - **`expert_capacity`**: — / — / — / **48** (2.2 block routing; new field). + - **`block_size`**: — / — / — / **32** (2.2 block routing; new field). + - `use_qk_norm`: (omitted, class-default True) … / **true** explicitly on 2.2. + - `transformers_version`: 4.57.1 / 4.57.1 / 4.51.0 / **5.2.0**; dtype key `torch_dtype`→`dtype` on the 4.57 configs. (So the trust_remote_code inference ceiling spans 4.51–5.2, not the stale 4.52.3 the repo's bundled config claimed.) + - mini vs flash size fields (`hidden_size` 2048/4096, `num_hidden_layers` 20/32, etc.) unchanged within a size class across generations. + - The 2.2 bundled **config class** additionally declares `expert_capacity=48`, `block_size=32`; the 2.0/2.1 bundled config classes are byte-identical. +- **Changes made:** + - `models/llada2_moe/configuration_llada2_moe.py`: declared `expert_capacity`/`block_size` as explicit first-class params (default `None`) instead of letting them be silently swallowed by `**kwargs`. + - `models/llada2_moe/compat.py` (**new, dependency-free**): `validate_llada2_config()` **fails loudly** (`ValueError`) on a non-LLaDA2 or structurally-inconsistent config (bad `first_k_dense_replace`, `num_experts<=0`, missing fields), and detects generation 2.0/2.1 vs **2.2 (block routing)**, returning blunt warnings. + - `scripts/moe_convertor.py`: refactored so `moe_merge`/`split_moe_experts` import **without** veomni (veomni now lazy-imported in `main()`); wired in `validate_llada2_config` + warning emission before conversion. + - New model-config dirs `configs/model_configs/{llada2_1_mini,llada2_1_flash,llada2_2_flash}/` (real architecture values, `model_type` set to the training alias `llada2_moe_veomni`; 2.2 carries expert_capacity/block_size/128K/rope_theta=3e6). + - New SFT configs `configs/sft/{llada2_1_mini,llada2_1_flash,llada2_2_flash}_bd_sft.yaml` (new nested schema; the 2.2 one carries prominent block-routing/128K warnings). +- **Verified ✅ (ran here, stdlib only):** validator gives correct generation/`block_routing`/context on all four **real** downloaded configs and on the three new training configs; all four negative cases raise; all new YAML/JSON parse; all changed `.py` pass `py_compile`. **Unverified 🟡:** actual weight load / `AutoConfig.from_pretrained` (needs transformers+torch) and end-to-end conversion of a real checkpoint (needs veomni + multi-GB weights). + +#### 4b — long-context ceiling 🟡 (documented; one guard added) +- **Data pipeline:** transforms pad/truncate each document to exactly `max_seq_len` (no multi-doc packing; `dyn_bsz=false` in longctx configs). One document per sample → no cross-document attention-boundary problem, but memory scales linearly with `max_seq_len × batch`; 128K needs `cp_size≥2` and/or activation offload. Functional, not packing-optimized. +- **Block-diffusion is the hard ceiling:** the block-diffusion attention mask is dense `(2·max_seq_len)²` and materialized on host (~16 GiB at 32k, ~256 GiB at 128k). It **cannot** be used for long context. The `configs/longctx/*` correctly set `block_diffusion_mode: false`; added a **loud runtime guard** in `_build_block_diffusion_mask` that warns past ~2 GiB. +- **RoPE / native context:** 2.2-flash is natively 128K (`rope_theta=3e6`), so no RoPE patch is needed up to 128K; `scripts/extend_rope_context.py` targets the mini models (native 32K) extended via YaRN/LongRoPE. Sequence-parallel (cp_size) and ring/ulysses attention are VeOmni's responsibility; dFactory only sets the sizes. flash_attention_2 is recommended over sdpa for very long context. +- **Real ceiling** (max trainable length on given hardware) depends on VeOmni CP + GPU memory and is **not verifiable here**. + +#### 4c — Levenshtein editing / block routing / L-EBPO ✅ (documented; stubs added, not wired) +- Wrote **`docs/UNIMPLEMENTED.md`** grounded in the LLaDA2.2 tech report (§3.1 Levenshtein editing with keep/substitute/DELETE/INSERT + LCS label construction; §3.2 L-EBPO agentic RL) **and** the concrete downloaded 2.2 `modeling_llada2_moe.py` (`block_routing()` selecting top-`expert_capacity`=48 of 256 experts per `block_size`=32 block; `_apply_edit_operations_with_tracking` DELETE/SPLIT inference ops; conditional `use_qk_norm`). +- Consequence stated bluntly: SFT'ing a 2.2 checkpoint here uses standard per-token top-k routing + plain masked/block-diffusion labels → trains **around** the editing/routing machinery and may degrade RL-trained editing behaviour. Block routing differs in the **forward pass**, so it affects training too, not just inference. +- Added **`models/llada2_moe/editing.py`**: `EditLabelConstructor` / `RoutingStrategy` interface stubs whose placeholder implementations **raise `NotImplementedError`** (verified) — never silent no-ops, and **not imported by the training path**. A real edit-label constructor could later plug into the `run_llada2_training(transform_builder=…)` hook. +- Did **not** implement any of these mechanisms (unpublished; explicitly out of scope per the brief). diff --git a/configs/model_configs/llada2_1_flash/config.json b/configs/model_configs/llada2_1_flash/config.json new file mode 100644 index 0000000..56ed46b --- /dev/null +++ b/configs/model_configs/llada2_1_flash/config.json @@ -0,0 +1,56 @@ +{ + "architectures": [ + "LLaDA2MoeModelLM" + ], + "attention_dropout": 0.0, + "auto_map": { + "AutoConfig": "configuration_llada2_moe.LLaDA2MoeConfig", + "AutoModel": "modeling_llada2_moe.LLaDA2MoeModel", + "AutoModelForCausalLM": "modeling_llada2_moe.LLaDA2MoeModelLM" + }, + "embedding_dropout": 0.0, + "first_k_dense_replace": 1, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 4096, + "initializer_range": 0.02, + "intermediate_size": 9216, + "max_position_embeddings": 32768, + "max_window_layers": 28, + "model_type": "llada2_moe_veomni", + "moe_intermediate_size": 1024, + "moe_router_enable_expert_bias": true, + "n_group": 8, + "norm_head": false, + "norm_softmax": false, + "norm_topk_prob": true, + "num_attention_heads": 32, + "num_experts": 256, + "num_experts_per_tok": 8, + "num_hidden_layers": 32, + "num_key_value_heads": 4, + "num_shared_experts": 1, + "output_dropout": 0.0, + "output_router_logits": false, + "pad_token_id": 156892, + "partial_rotary_factor": 0.5, + "rms_norm_eps": 1e-06, + "rope_scaling": null, + "rope_theta": 600000, + "rotary_dim": 64, + "routed_scaling_factor": 2.5, + "router_dtype": "fp32", + "score_function": "sigmoid", + "sliding_window": 4096, + "tie_word_embeddings": false, + "topk_group": 4, + "torch_dtype": "bfloat16", + "transformers_version": "4.51.0", + "use_bias": false, + "use_cache": false, + "use_qkv_bias": false, + "use_rmsnorm": true, + "use_sliding_window": false, + "using_split_qkv_in_self_attention": false, + "vocab_size": 157184 +} \ No newline at end of file diff --git a/configs/model_configs/llada2_1_flash/configuration_llada2_moe.py b/configs/model_configs/llada2_1_flash/configuration_llada2_moe.py new file mode 100644 index 0000000..1151aa4 --- /dev/null +++ b/configs/model_configs/llada2_1_flash/configuration_llada2_moe.py @@ -0,0 +1,88 @@ +"""LLaDA2 MoE model configuration""" + +from transformers.configuration_utils import PretrainedConfig + + +class LLaDA2MoeConfig(PretrainedConfig): + model_type = "llada2_moe" + + def __init__( + self, + vocab_size=30592, + hidden_size=1024, + intermediate_size=None, + num_hidden_layers=24, + num_attention_heads=16, + num_key_value_heads=0, + hidden_act="silu", + use_qkv_bias=False, # llada2 only + use_qk_norm=True, + use_bias=True, # llada2 only + rms_norm_eps=1e-05, + norm_head=False, # llada2 only + tie_word_embeddings=False, # PretrainedConfig key, here change default value. + embedding_dropout=0.1, + attention_dropout=0.1, + output_dropout=0.1, + initializer_range=0.02, + max_position_embeddings=16384, + rope_theta=10000.0, + use_cache=True, + use_sliding_window=False, + sliding_window=4096, + max_window_layers=28, + rope_scaling=None, + pad_token_id=126081, + num_experts=16, + num_shared_experts=0, + num_experts_per_tok=2, + n_group=8, + topk_group=4, + routed_scaling_factor=2.5, + moe_intermediate_size=None, + first_k_dense_replace=0, + head_dim=None, + output_router_logits=False, + partial_rotary_factor=0.5, + **kwargs, + ): + self.num_hidden_layers = num_hidden_layers + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_attention_heads = num_attention_heads + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.use_qkv_bias = use_qkv_bias + self.use_qk_norm = use_qk_norm + self.use_bias = use_bias + self.norm_head = norm_head + self.rms_norm_eps = rms_norm_eps + self.embedding_dropout = embedding_dropout + self.attention_dropout = attention_dropout + self.output_dropout = output_dropout + self.initializer_range = initializer_range + self.max_position_embeddings = max_position_embeddings + self.rope_theta = rope_theta + self.use_cache = use_cache + self.use_sliding_window = use_sliding_window + self.sliding_window = sliding_window + self.max_window_layers = max_window_layers + self.head_dim = head_dim or self.hidden_size // self.num_attention_heads + self.rope_scaling = rope_scaling + + # MoE configs + self.num_experts = num_experts + self.num_shared_experts = num_shared_experts + self.num_experts_per_tok = num_experts_per_tok + self.n_group = n_group + self.topk_group = topk_group + self.moe_intermediate_size = moe_intermediate_size + self.first_k_dense_replace = first_k_dense_replace + self.output_router_logits = output_router_logits + self.routed_scaling_factor = routed_scaling_factor + self.partial_rotary_factor = partial_rotary_factor + + super().__init__( + pad_token_id=pad_token_id, tie_word_embeddings=tie_word_embeddings, **kwargs + ) \ No newline at end of file diff --git a/configs/model_configs/llada2_1_mini/config.json b/configs/model_configs/llada2_1_mini/config.json new file mode 100644 index 0000000..e90af66 --- /dev/null +++ b/configs/model_configs/llada2_1_mini/config.json @@ -0,0 +1,56 @@ +{ + "architectures": [ + "LLaDA2MoeModelLM" + ], + "attention_dropout": 0.0, + "auto_map": { + "AutoConfig": "configuration_llada2_moe.LLaDA2MoeConfig", + "AutoModel": "modeling_llada2_moe.LLaDA2MoeModel", + "AutoModelForCausalLM": "modeling_llada2_moe.LLaDA2MoeModelLM" + }, + "dtype": "bfloat16", + "embedding_dropout": 0.0, + "first_k_dense_replace": 1, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2048, + "initializer_range": 0.02, + "intermediate_size": 5120, + "max_position_embeddings": 32768, + "max_window_layers": 28, + "model_type": "llada2_moe_veomni", + "moe_intermediate_size": 512, + "moe_router_enable_expert_bias": true, + "n_group": 8, + "norm_head": false, + "norm_softmax": false, + "norm_topk_prob": true, + "num_attention_heads": 16, + "num_experts": 256, + "num_experts_per_tok": 8, + "num_hidden_layers": 20, + "num_key_value_heads": 4, + "num_shared_experts": 1, + "output_dropout": 0.0, + "output_router_logits": false, + "pad_token_id": 156892, + "partial_rotary_factor": 0.5, + "rms_norm_eps": 1e-06, + "rope_scaling": null, + "rope_theta": 600000, + "rotary_dim": 64, + "routed_scaling_factor": 2.5, + "router_dtype": "fp32", + "score_function": "sigmoid", + "sliding_window": 4096, + "tie_word_embeddings": false, + "topk_group": 4, + "transformers_version": "4.57.1", + "use_bias": false, + "use_cache": false, + "use_qkv_bias": false, + "use_rmsnorm": true, + "use_sliding_window": false, + "using_split_qkv_in_self_attention": false, + "vocab_size": 157184 +} \ No newline at end of file diff --git a/configs/model_configs/llada2_1_mini/configuration_llada2_moe.py b/configs/model_configs/llada2_1_mini/configuration_llada2_moe.py new file mode 100644 index 0000000..1151aa4 --- /dev/null +++ b/configs/model_configs/llada2_1_mini/configuration_llada2_moe.py @@ -0,0 +1,88 @@ +"""LLaDA2 MoE model configuration""" + +from transformers.configuration_utils import PretrainedConfig + + +class LLaDA2MoeConfig(PretrainedConfig): + model_type = "llada2_moe" + + def __init__( + self, + vocab_size=30592, + hidden_size=1024, + intermediate_size=None, + num_hidden_layers=24, + num_attention_heads=16, + num_key_value_heads=0, + hidden_act="silu", + use_qkv_bias=False, # llada2 only + use_qk_norm=True, + use_bias=True, # llada2 only + rms_norm_eps=1e-05, + norm_head=False, # llada2 only + tie_word_embeddings=False, # PretrainedConfig key, here change default value. + embedding_dropout=0.1, + attention_dropout=0.1, + output_dropout=0.1, + initializer_range=0.02, + max_position_embeddings=16384, + rope_theta=10000.0, + use_cache=True, + use_sliding_window=False, + sliding_window=4096, + max_window_layers=28, + rope_scaling=None, + pad_token_id=126081, + num_experts=16, + num_shared_experts=0, + num_experts_per_tok=2, + n_group=8, + topk_group=4, + routed_scaling_factor=2.5, + moe_intermediate_size=None, + first_k_dense_replace=0, + head_dim=None, + output_router_logits=False, + partial_rotary_factor=0.5, + **kwargs, + ): + self.num_hidden_layers = num_hidden_layers + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_attention_heads = num_attention_heads + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.use_qkv_bias = use_qkv_bias + self.use_qk_norm = use_qk_norm + self.use_bias = use_bias + self.norm_head = norm_head + self.rms_norm_eps = rms_norm_eps + self.embedding_dropout = embedding_dropout + self.attention_dropout = attention_dropout + self.output_dropout = output_dropout + self.initializer_range = initializer_range + self.max_position_embeddings = max_position_embeddings + self.rope_theta = rope_theta + self.use_cache = use_cache + self.use_sliding_window = use_sliding_window + self.sliding_window = sliding_window + self.max_window_layers = max_window_layers + self.head_dim = head_dim or self.hidden_size // self.num_attention_heads + self.rope_scaling = rope_scaling + + # MoE configs + self.num_experts = num_experts + self.num_shared_experts = num_shared_experts + self.num_experts_per_tok = num_experts_per_tok + self.n_group = n_group + self.topk_group = topk_group + self.moe_intermediate_size = moe_intermediate_size + self.first_k_dense_replace = first_k_dense_replace + self.output_router_logits = output_router_logits + self.routed_scaling_factor = routed_scaling_factor + self.partial_rotary_factor = partial_rotary_factor + + super().__init__( + pad_token_id=pad_token_id, tie_word_embeddings=tie_word_embeddings, **kwargs + ) \ No newline at end of file diff --git a/configs/model_configs/llada2_2_flash/config.json b/configs/model_configs/llada2_2_flash/config.json new file mode 100644 index 0000000..3f6fc8b --- /dev/null +++ b/configs/model_configs/llada2_2_flash/config.json @@ -0,0 +1,58 @@ +{ + "architectures": [ + "LLaDA2MoeModelLM" + ], + "attention_dropout": 0.0, + "auto_map": { + "AutoConfig": "configuration_llada2_moe.LLaDA2MoeConfig", + "AutoModel": "modeling_llada2_moe.LLaDA2MoeModel", + "AutoModelForCausalLM": "modeling_llada2_moe.LLaDA2MoeModelLM" + }, + "num_hidden_layers": 32, + "hidden_size": 4096, + "intermediate_size": 9216, + "first_k_dense_replace": 1, + "hidden_act": "silu", + "max_position_embeddings": 131072, + "model_type": "llada2_moe_veomni", + "moe_intermediate_size": 1024, + "norm_topk_prob": true, + "num_experts_per_tok": 8, + "expert_capacity": 48, + "block_size": 32, + "norm_head": false, + "num_attention_heads": 32, + "num_experts": 256, + "num_key_value_heads": 4, + "rope_theta": 3000000, + "rope_scaling": null, + "tie_word_embeddings": false, + "torch_dtype": "bfloat16", + "transformers_version": "5.2.0", + "use_bias": false, + "use_rmsnorm": true, + "rms_norm_eps": 1e-06, + "head_dim": 128, + "num_shared_experts": 1, + "use_cache": false, + "use_qk_norm": true, + "use_qkv_bias": false, + "embedding_dropout": 0.0, + "norm_softmax": false, + "output_dropout": 0.0, + "vocab_size": 157184, + "rotary_dim": 64, + "using_split_qkv_in_self_attention": false, + "router_dtype": "fp32", + "moe_router_enable_expert_bias": true, + "routed_scaling_factor": 2.5, + "n_group": 8, + "topk_group": 4, + "score_function": "sigmoid", + "initializer_range": 0.02, + "max_window_layers": 28, + "output_router_logits": false, + "pad_token_id": 156892, + "partial_rotary_factor": 0.5, + "use_sliding_window": false +} \ No newline at end of file diff --git a/configs/model_configs/llada2_2_flash/configuration_llada2_moe.py b/configs/model_configs/llada2_2_flash/configuration_llada2_moe.py new file mode 100644 index 0000000..2540bc1 --- /dev/null +++ b/configs/model_configs/llada2_2_flash/configuration_llada2_moe.py @@ -0,0 +1,94 @@ +"""LLaDA2 MoE model configuration""" + +from transformers.configuration_utils import PretrainedConfig + + +class LLaDA2MoeConfig(PretrainedConfig): + model_type = "llada2_moe" + + def __init__( + self, + vocab_size=30592, + hidden_size=1024, + intermediate_size=None, + num_hidden_layers=24, + num_attention_heads=16, + num_key_value_heads=0, + hidden_act="silu", + use_qkv_bias=False, # llada2 only + use_qk_norm=False, + use_bias=True, # llada2 only + rms_norm_eps=1e-05, + norm_head=False, # llada2 only + tie_word_embeddings=False, # PretrainedConfig key, here change default value. + embedding_dropout=0.1, + attention_dropout=0.1, + output_dropout=0.1, + initializer_range=0.02, + max_position_embeddings=16384, + rope_theta=10000.0, + use_cache=True, + use_sliding_window=False, + sliding_window=4096, + max_window_layers=28, + rope_scaling=None, + pad_token_id=126081, + num_experts=16, + num_shared_experts=0, + num_experts_per_tok=2, + n_group=8, + topk_group=4, + routed_scaling_factor=2.5, + moe_intermediate_size=None, + first_k_dense_replace=0, + head_dim=None, + output_router_logits=False, + partial_rotary_factor=0.5, + expert_capacity=48, + block_size=32, + **kwargs, + ): + self.num_hidden_layers = num_hidden_layers + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_attention_heads = num_attention_heads + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.use_qkv_bias = use_qkv_bias + self.use_qk_norm = use_qk_norm + self.use_bias = use_bias + self.norm_head = norm_head + self.rms_norm_eps = rms_norm_eps + self.embedding_dropout = embedding_dropout + self.attention_dropout = attention_dropout + self.output_dropout = output_dropout + self.initializer_range = initializer_range + self.max_position_embeddings = max_position_embeddings + self.rope_theta = rope_theta + self.use_cache = use_cache + self.use_sliding_window = use_sliding_window + self.sliding_window = sliding_window + self.max_window_layers = max_window_layers + self.head_dim = head_dim or self.hidden_size // self.num_attention_heads + self.rope_scaling = rope_scaling + + # MoE configs + self.num_experts = num_experts + self.num_shared_experts = num_shared_experts + self.num_experts_per_tok = num_experts_per_tok + self.n_group = n_group + self.topk_group = topk_group + self.moe_intermediate_size = moe_intermediate_size + self.first_k_dense_replace = first_k_dense_replace + self.output_router_logits = output_router_logits + self.routed_scaling_factor = routed_scaling_factor + self.partial_rotary_factor = partial_rotary_factor + + # Block routing configs + self.expert_capacity = expert_capacity + self.block_size = block_size + + super().__init__( + pad_token_id=pad_token_id, tie_word_embeddings=tie_word_embeddings, **kwargs + ) diff --git a/configs/sft/llada2_1_flash_bd_sft.yaml b/configs/sft/llada2_1_flash_bd_sft.yaml new file mode 100644 index 0000000..94bd44c --- /dev/null +++ b/configs/sft/llada2_1_flash_bd_sft.yaml @@ -0,0 +1,81 @@ +# SFT config for LLaDA2.1-flash (100B). Same architecture/size as 2.0-flash +# (32 layers, hidden 4096, 256 experts). Mirrors llada2_flash_bd_sft.yaml. +# +# Prereq: merge experts first (see README): +# python scripts/moe_convertor.py -i ./LLaDA2.1-flash -o ./LLaDA2.1-flash-moe-merge -m merge +model: + config_path: ./configs/model_configs/llada2_1_flash + model_path: ./LLaDA2.1-flash-moe-merge + tokenizer_path: ./LLaDA2.1-flash-moe-merge + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton + cross_entropy_loss_implementation: eager + rms_norm_implementation: eager + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: eager + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager + +data: + train_path: ./gsm8k_datasets/gsm8k_train.jsonl + data_type: conversation + datasets_type: mapping + max_seq_len: 2048 + text_keys: messages + noise_range_low: 0.3 + noise_range_high: 0.8 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true + +train: + dyn_bsz: false + global_batch_size: 16 + micro_batch_size: 1 + num_train_epochs: 1 + bsz_warmup_ratio: 0.007 + init_device: meta + broadcast_model_weights_from_rank0: true + enable_full_determinism: false + empty_cache_steps: 500 + beta1: 0.9 + beta2: 0.999 + block_diffusion_mode: true + block_size: 32 + same_token_labels: true + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_1_flash_bd_sft_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/sft/llada2_1_mini_bd_sft.yaml b/configs/sft/llada2_1_mini_bd_sft.yaml new file mode 100644 index 0000000..99d0103 --- /dev/null +++ b/configs/sft/llada2_1_mini_bd_sft.yaml @@ -0,0 +1,81 @@ +# SFT config for LLaDA2.1-mini (16B). Same architecture/size as 2.0-mini +# (20 layers, hidden 2048, 256 experts). Mirrors llada2_mini_bd_sft.yaml. +# +# Prereq: merge experts first (see README): +# python scripts/moe_convertor.py -i ./LLaDA2.1-mini -o ./LLaDA2.1-mini-moe-merge -m merge +model: + config_path: ./configs/model_configs/llada2_1_mini + model_path: ./LLaDA2.1-mini-moe-merge + tokenizer_path: ./LLaDA2.1-mini-moe-merge + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton + cross_entropy_loss_implementation: eager + rms_norm_implementation: eager + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: eager + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager + +data: + train_path: ./gsm8k_datasets/gsm8k_train.jsonl + data_type: conversation + datasets_type: mapping + max_seq_len: 2048 + text_keys: messages + noise_range_low: 0.3 + noise_range_high: 0.8 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true + +train: + dyn_bsz: false + global_batch_size: 8 + micro_batch_size: 1 + num_train_epochs: 1 + bsz_warmup_ratio: 0.007 + init_device: meta + broadcast_model_weights_from_rank0: true + enable_full_determinism: false + empty_cache_steps: 500 + beta1: 0.9 + beta2: 0.999 + block_diffusion_mode: true + block_size: 32 + same_token_labels: true + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_1_mini_bd_sft_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/configs/sft/llada2_2_flash_bd_sft.yaml b/configs/sft/llada2_2_flash_bd_sft.yaml new file mode 100644 index 0000000..cf40db9 --- /dev/null +++ b/configs/sft/llada2_2_flash_bd_sft.yaml @@ -0,0 +1,94 @@ +# SFT config for LLaDA2.2-flash (100B). READ THIS BEFORE TRAINING: +# +# ┌──────────────────────────────────────────────────────────────────────────────────┐ +# │ dFactory does NOT implement LLaDA2.2's block routing (top-C=48 of E=256 experts │ +# │ per block; config: expert_capacity=48, block_size=32) nor its Levenshtein │ +# │ DELETE/INSERT editing supervision or L-EBPO RL. This config trains 2.2 with the │ +# │ SAME standard per-token top-k masked/block-diffusion objective used for 2.0/2.1. │ +# │ That means SFT here trains AROUND the 2.2 routing/editing machinery and may │ +# │ degrade the model's RL-trained editing behaviour. See docs/UNIMPLEMENTED.md. │ +# └──────────────────────────────────────────────────────────────────────────────────┘ +# +# 2.2-flash is natively 128k context (max_position_embeddings=131072, rope_theta=3e6). +# This config uses a short max_seq_len (2048) for a cheap SFT smoke run. For long-context +# SFT, raise data.max_seq_len and accelerator.cp_size together (see configs/longctx/* +# and MIGRATION_NOTES.md Phase 4b for the ceiling/verification status). +# +# Prereq: merge experts first (see README): +# python scripts/moe_convertor.py -i ./LLaDA2.2-flash -o ./LLaDA2.2-flash-moe-merge -m merge +model: + config_path: ./configs/model_configs/llada2_2_flash + model_path: ./LLaDA2.2-flash-moe-merge + tokenizer_path: ./LLaDA2.2-flash-moe-merge + ops_implementation: + attn_implementation: sdpa + moe_implementation: fused_triton + cross_entropy_loss_implementation: eager + rms_norm_implementation: eager + swiglu_mlp_implementation: eager + rotary_pos_emb_implementation: eager + rotary_pos_emb_vision_implementation: eager + load_balancing_loss_implementation: eager + rms_norm_gated_implementation: eager + causal_conv1d_implementation: eager + chunk_gated_delta_rule_implementation: eager + +data: + train_path: ./gsm8k_datasets/gsm8k_train.jsonl + data_type: conversation + datasets_type: mapping + max_seq_len: 2048 # native ctx is 131072; raise + set cp_size for long-context SFT + text_keys: messages + noise_range_low: 0.3 + noise_range_high: 0.8 + dataloader: + type: native + num_workers: 16 + drop_last: true + pin_memory: true + +train: + dyn_bsz: false + global_batch_size: 16 + micro_batch_size: 1 + num_train_epochs: 1 + bsz_warmup_ratio: 0.007 + init_device: meta + broadcast_model_weights_from_rank0: true + enable_full_determinism: false + empty_cache_steps: 500 + beta1: 0.9 + beta2: 0.999 + block_diffusion_mode: true + block_size: 32 # block-diffusion attention block; NOT 2.2 expert-block routing + same_token_labels: true + optimizer: + type: adamw + lr: 1.0e-5 + lr_warmup_ratio: 0.03 + lr_decay_style: cosine + lr_decay_ratio: 1.0 + weight_decay: 0.1 + max_grad_norm: 1.0 + accelerator: + tp_size: 1 + ep_size: 1 + pp_size: 1 + ulysses_size: 1 + cp_size: 1 # raise to >=2 (and increase max_seq_len) for long-context SFT + fsdp_config: + fsdp_mode: fsdp2 + offload: true + mixed_precision: + enable: true + gradient_checkpointing: + enable: true + enable_reentrant: false + checkpoint: + output_dir: ./llada2_2_flash_bd_sft_outputs + manager: dcp + load_path: null + save_epochs: 1 + save_hf_weights: true + wandb: + enable: false diff --git a/docs/UNIMPLEMENTED.md b/docs/UNIMPLEMENTED.md new file mode 100644 index 0000000..e570ba1 --- /dev/null +++ b/docs/UNIMPLEMENTED.md @@ -0,0 +1,88 @@ +# What dFactory does NOT implement for LLaDA2.2 + +dFactory can **load, convert, and SFT** LLaDA2.2 checkpoints (Phase 4a), but it trains them +with the **same masked / block-diffusion objective and standard per-token top-k MoE routing** +used for 2.0/2.1. Several LLaDA2.2 training-time mechanisms are **unpublished (weights only, no +training code)** and are **not** implemented here. This file states precisely what is missing, +the evidence, and the consequence — so you do not burn GPU hours on a false assumption. + +> **Bottom line:** SFT'ing a 2.2 checkpoint with this repo trains *around* its editing/routing +> machinery. It will not teach or preserve DELETE/INSERT editing, and it does not reproduce +> 2.2's block routing. It can still be a valid plain-diffusion SFT, but it may **degrade** the +> RL-trained editing behaviour baked into the released 2.2 weights. + +Evidence sources: the LLaDA2.2 tech report (§3.1, §3.2) and the checkpoint's own bundled +inference file `modeling_llada2_moe.py` (downloaded from `inclusionAI/LLaDA2.2-flash`), compared +against this repo's training model `models/llada2_moe/modeling_llada2_moe.py`. + +--- + +## 1. Levenshtein editing: LCS-based edit-label construction (paper §3.1) + +**What 2.2 does.** Where earlier text-to-text correction only allows position-wise *keep* and +*substitute* (fixed length), LLaDA2.2 adds two more operations — *delete* and *insert* — via two +new edit-control tokens (DELETE, INSERT). Training supervision comes from **LCS-based edit-label +construction**: the target edit at each position/noise level is derived by Levenshtein-aligning +the (noised) draft against the ground-truth sequence. + +**What dFactory has.** `tasks/dataset/data_transform.py` builds labels by **random masking only** +(`sft_noise_transition` flips tokens to a single mask id; `labels[~is_mask] = -100`). There is: +- no LCS / Levenshtein alignment, +- no DELETE/INSERT edit-control-token supervision, +- no variable-length edit targets. + +The DELETE/INSERT tokens exist in the tokenizer/vocab (the checkpoint's bundled model implements +edit-op *inference* — see `_apply_edit_operations_with_tracking` in the 2.2 modeling file), but the +**training-time label construction that teaches them is absent** and unpublished. + +## 2. Block routing (top-C of E experts per block) + +**What 2.2 does.** Config adds `expert_capacity: 48`, `block_size: 32`. The bundled 2.2 model's +`LLaDA2MoeGate.block_routing()` reshapes router scores to `(num_blocks, block_size, num_experts)`, +takes the per-block max over positions, selects the **top `expert_capacity` (=48) experts of +`num_experts` (=256) for the whole block**, and masks all other experts for every token in that +block. This "token-racing" block-level admission cuts MoE inference cost for long-context agents. + +**What dFactory has.** `models/llada2_moe/modeling_llada2_moe.py` routes **per token, top-`k` +(=`num_experts_per_tok`=8)** via `LLaDA2MoeGate` + `fused_moe_forward`. It **ignores** +`expert_capacity`/`block_size` entirely (they are carried as config attributes for detection/ +round-trip only). Because routing is part of the **forward pass**, this differs during *both* +training and inference: training a 2.2 checkpoint here optimizes a different expert-selection +distribution than the one the weights were trained under. + +## 3. L-EBPO agentic RL (paper §3.2) + +**What 2.2 does.** L-EBPO (Levenshtein-Editing ELBO-based Block-level Policy Optimization) is an +agentic RL algorithm that combines LCS-derived edit labels with environmental reward signals to +optimize editing/error-correction decisions in tool-use rollouts. + +**What dFactory has.** Nothing. dFactory is an **SFT** framework (masked/block diffusion). There +is no RL loop, no reward model, no rollout/environment interface, no EBPO objective. (Upstream's +`add_tpd` branch adds *trainable parallel decoding*, already merged — that is unrelated to RL.) + +--- + +## Also note: `use_qk_norm` + +2.2's config sets `use_qk_norm: true`, and its bundled model applies query/key RMSNorm **only when +that flag is set**. dFactory's training model applies query/key RMSNorm **unconditionally** (it +never reads `use_qk_norm`). This happens to match all current released checkpoints (2.0/2.1 default +the flag on via their config class too), so it is correct for them — but it is a latent mismatch if +a future checkpoint ships `use_qk_norm: false`. Tracked, not fixed (changing the validated forward +was out of scope for this pass). + +--- + +## Extension points (unimplemented stubs, not wired in) + +To give a future implementation a clear contract without pretending anything works, this repo +provides **interface stubs that raise `NotImplementedError`** (never a silent no-op): + +- `models/llada2_moe/editing.py` — `EditLabelConstructor` (LCS/Levenshtein edit-label hook) and + `RoutingStrategy` (block-routing hook). Neither is imported by the training path. +- The SFT loop already exposes real, working hooks (`run_llada2_training(transform_builder=…, + dataset_builder=…)` in `tasks/train_llada2_common.py`) that a real `EditLabelConstructor` could + plug into once implemented. + +Do **not** treat these stubs as functional. Implementing them requires the unpublished algorithms +above and is explicitly out of scope for this modernization. diff --git a/models/llada2_moe/compat.py b/models/llada2_moe/compat.py new file mode 100644 index 0000000..0c9cfab --- /dev/null +++ b/models/llada2_moe/compat.py @@ -0,0 +1,126 @@ +"""LLaDA2 checkpoint compatibility validation. + +Deliberately dependency-free (no torch / transformers / veomni imports) so it can be +imported and unit-tested standalone, and reused by ``scripts/moe_convertor.py`` and the +training entrypoints. + +Two jobs: + 1. ``validate_llada2_config`` — fail LOUDLY if a config is not a recognizable LLaDA2 MoE + architecture, or is internally inconsistent, rather than proceeding on wrong assumptions. + 2. Detect the LLaDA2 generation (2.0/2.1 vs 2.2 block-routing) and surface blunt warnings + about the mechanisms dFactory does NOT implement (see ``docs/UNIMPLEMENTED.md``). +""" +from __future__ import annotations + +import sys +from typing import Any, Dict, List + +# The config-class strings all LLaDA2 checkpoints share, and the training-side alias. +LLADA2_MODEL_TYPES = {"llada2_moe", "llada2_moe_veomni"} +LLADA2_ARCH_MARKER = "LLaDA2Moe" + + +def _get(config: Any, key: str, default: Any = None) -> Any: + """Read ``key`` from either a mapping (config.json dict) or an object (PretrainedConfig).""" + if isinstance(config, dict): + return config.get(key, default) + return getattr(config, key, default) + + +def _require(config: Any, key: str, problems: List[str]) -> Any: + val = _get(config, key, None) + if val is None: + problems.append(f"missing required field '{key}'") + return val + + +def validate_llada2_config(config: Any) -> Dict[str, Any]: + """Validate an LLaDA2 MoE config; raise ``ValueError`` if it is not one. + + Returns a description dict:: + + {"generation": "2.0/2.1" | "2.2", "block_routing": bool, "num_experts": int, + "context_length": int | None, "warnings": [str, ...]} + + The caller is responsible for surfacing ``warnings`` (see :func:`emit_warnings`). + """ + # --- 1. Is this actually a LLaDA2 MoE checkpoint? ----------------------------------- + model_type = _get(config, "model_type", None) + architectures = _get(config, "architectures", None) or [] + arch_ok = any(LLADA2_ARCH_MARKER in str(a) for a in architectures) + type_ok = model_type in LLADA2_MODEL_TYPES + + if not (arch_ok or type_ok): + raise ValueError( + "Not a recognized LLaDA2 MoE checkpoint: " + f"model_type={model_type!r}, architectures={architectures!r}. " + f"Expected model_type in {sorted(LLADA2_MODEL_TYPES)} or an architecture " + f"containing {LLADA2_ARCH_MARKER!r}. Refusing to proceed with wrong assumptions." + ) + + # --- 2. Structural consistency (the fields moe_convertor / the model rely on) -------- + problems: List[str] = [] + num_layers = _require(config, "num_hidden_layers", problems) + num_experts = _require(config, "num_experts", problems) + moe_inter = _require(config, "moe_intermediate_size", problems) + _require(config, "hidden_size", problems) + _require(config, "num_experts_per_tok", problems) + first_k = _get(config, "first_k_dense_replace", 0) + + if num_experts is not None and num_experts <= 0: + problems.append(f"num_experts must be > 0 (got {num_experts})") + if moe_inter is not None and moe_inter <= 0: + problems.append(f"moe_intermediate_size must be > 0 (got {moe_inter})") + if num_layers is not None and first_k is not None: + if not (0 <= first_k < num_layers): + problems.append( + f"first_k_dense_replace must satisfy 0 <= v < num_hidden_layers " + f"(got first_k_dense_replace={first_k}, num_hidden_layers={num_layers}); " + "there would be no MoE layers to convert" + ) + if problems: + raise ValueError( + "LLaDA2 config failed structural validation:\n - " + "\n - ".join(problems) + ) + + # --- 3. Generation detection + blunt capability warnings ---------------------------- + warnings: List[str] = [] + expert_capacity = _get(config, "expert_capacity", None) + block_size = _get(config, "block_size", None) + is_block_routing = expert_capacity is not None or block_size is not None + generation = "2.2" if is_block_routing else "2.0/2.1" + + if is_block_routing: + warnings.append( + "This looks like a LLaDA2.2 checkpoint (block routing: " + f"expert_capacity={expert_capacity}, block_size={block_size}). " + "dFactory's training MoE uses standard per-token top-k routing and does NOT " + "implement block routing (top-C of E experts per block). SFT here will train " + "AROUND the 2.2 routing/editing machinery and may degrade RL-trained editing " + "behaviour. See docs/UNIMPLEMENTED.md before spending GPU hours." + ) + + ctx = _get(config, "max_position_embeddings", None) + if ctx is not None and ctx >= 65536: + warnings.append( + f"Long native context (max_position_embeddings={ctx}). Verify the data " + "pipeline / packing and set context_parallel (cp_size) accordingly; the " + "2.0-era configs assumed <= 32k. See MIGRATION_NOTES.md (Phase 4b)." + ) + + return { + "generation": generation, + "block_routing": is_block_routing, + "num_experts": num_experts, + "context_length": ctx, + "warnings": warnings, + } + + +def emit_warnings(info: Dict[str, Any], stream=sys.stderr) -> None: + """Print any warnings from :func:`validate_llada2_config` prominently.""" + for w in info.get("warnings", []): + print(f"[LLaDA2 WARNING] {w}", file=stream) + + +__all__ = ["validate_llada2_config", "emit_warnings", "LLADA2_MODEL_TYPES"] diff --git a/models/llada2_moe/configuration_llada2_moe.py b/models/llada2_moe/configuration_llada2_moe.py index bb2d0ee..8a19fba 100644 --- a/models/llada2_moe/configuration_llada2_moe.py +++ b/models/llada2_moe/configuration_llada2_moe.py @@ -44,6 +44,13 @@ def __init__( head_dim=None, output_router_logits=False, partial_rotary_factor=0.5, + # LLaDA2.2 block-routing fields (absent/None on 2.0/2.1). Declared explicitly so + # they are first-class attributes rather than silently swallowed by **kwargs, and + # so validation/tooling can detect a 2.2 checkpoint. NOTE: dFactory's training + # modeling does NOT implement block routing (see docs/UNIMPLEMENTED.md) — these are + # carried for detection/round-trip fidelity, not consumed by the training forward. + expert_capacity=None, + block_size=None, **kwargs, ): self.num_hidden_layers = num_hidden_layers @@ -82,6 +89,10 @@ def __init__( self.routed_scaling_factor = routed_scaling_factor self.partial_rotary_factor = partial_rotary_factor + # Block-routing configs (LLaDA2.2). None => not a block-routing checkpoint. + self.expert_capacity = expert_capacity + self.block_size = block_size + super().__init__(pad_token_id=pad_token_id, tie_word_embeddings=tie_word_embeddings, **kwargs) diff --git a/models/llada2_moe/editing.py b/models/llada2_moe/editing.py new file mode 100644 index 0000000..9ccd111 --- /dev/null +++ b/models/llada2_moe/editing.py @@ -0,0 +1,81 @@ +"""UNIMPLEMENTED extension-point stubs for LLaDA2.2 training-time mechanisms. + +These are **interface contracts only**. Every method raises ``NotImplementedError`` — never a +silent no-op. Nothing here is imported by the training path; a future implementation of the +unpublished LLaDA2.2 algorithms can fill these in and wire them via the existing +``run_llada2_training(transform_builder=..., dataset_builder=...)`` hooks. + +See ``docs/UNIMPLEMENTED.md`` for exactly what is missing and why. Do NOT use these as if they work. + +Dependency-free by design (no torch/veomni import) so the contract stays readable and testable. +""" +from __future__ import annotations + +from typing import Any, Dict, Protocol, Sequence, runtime_checkable + + +@runtime_checkable +class EditLabelConstructor(Protocol): + """Contract for LCS/Levenshtein edit-label construction (paper §3.1). + + A real implementation would align a (noised) draft against the ground-truth sequence and emit + per-position keep/substitute/DELETE/INSERT supervision instead of plain masked-token labels. + Intended to be adapted into a ``transform_builder`` for ``run_llada2_training``. + """ + + def build_edit_labels( + self, + draft_ids: Sequence[int], + target_ids: Sequence[int], + *, + delete_token_id: int, + insert_token_id: int, + ) -> Dict[str, Any]: + ... + + +class NotImplementedEditLabelConstructor: + """Placeholder that refuses to run. Replace with a real LCS-based constructor.""" + + def build_edit_labels(self, draft_ids, target_ids, *, delete_token_id, insert_token_id): + raise NotImplementedError( + "LCS-based Levenshtein edit-label construction (LLaDA2.2 paper §3.1) is not " + "implemented. dFactory trains plain masked/block diffusion. See docs/UNIMPLEMENTED.md." + ) + + +@runtime_checkable +class RoutingStrategy(Protocol): + """Contract for MoE expert routing. + + The default dFactory path is per-token top-k (implemented directly in the model, not through + this interface). LLaDA2.2 block routing (top-``expert_capacity`` of ``num_experts`` per block + of ``block_size`` tokens) would implement this contract. + """ + + def select_experts(self, router_scores, *, num_experts_per_tok: int) -> Any: + ... + + +class BlockRoutingStrategy: + """Placeholder for LLaDA2.2 block routing. Refuses to run.""" + + def __init__(self, expert_capacity: int, block_size: int, num_experts: int): + self.expert_capacity = expert_capacity + self.block_size = block_size + self.num_experts = num_experts + + def select_experts(self, router_scores, *, num_experts_per_tok: int): + raise NotImplementedError( + "LLaDA2.2 block routing (top-C=%d of E=%d experts per block of %d tokens) is not " + "implemented in dFactory's training MoE (per-token top-k only). See docs/UNIMPLEMENTED.md." + % (self.expert_capacity, self.num_experts, self.block_size) + ) + + +__all__ = [ + "EditLabelConstructor", + "NotImplementedEditLabelConstructor", + "RoutingStrategy", + "BlockRoutingStrategy", +] diff --git a/scripts/moe_convertor.py b/scripts/moe_convertor.py index 0139f32..b4b4422 100644 --- a/scripts/moe_convertor.py +++ b/scripts/moe_convertor.py @@ -1,3 +1,4 @@ +import importlib.util import os import re from argparse import ArgumentParser @@ -10,7 +11,16 @@ from tqdm import tqdm from transformers import AutoConfig -from veomni.models import build_tokenizer, save_model_weights +# Load the dependency-free LLaDA2 validator directly by file path. This avoids importing +# the `models.llada2_moe` package (whose __init__ pulls in torch+veomni for training-time +# self-registration), so the merge/split logic below stays importable & unit-testable +# without a full VeOmni install. veomni itself is imported lazily inside main(). +_COMPAT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "models", "llada2_moe", "compat.py") +_spec = importlib.util.spec_from_file_location("llada2_compat", _COMPAT_PATH) +_compat = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_compat) +validate_llada2_config = _compat.validate_llada2_config +emit_warnings = _compat.emit_warnings @dataclass @@ -30,7 +40,7 @@ def __iter__(self) -> Generator[tuple[str, torch.Tensor], None, None]: def moe_merge(state_dict: dict[str, torch.Tensor], config) -> dict[str, torch.Tensor]: new_state_dict: dict[str, torch.Tensor] = dict() - processed_keys: set[str] = set() + processed_keys: set[str] = set() num_layers = config.num_hidden_layers num_experts = config.num_experts @@ -42,7 +52,7 @@ def moe_merge(state_dict: dict[str, torch.Tensor], config) -> dict[str, torch.Te for layer_id in range(first_k_dense_replace, num_layers): for proj_type in proj_types: expert_weights = [] - current_expert_keys = [] + current_expert_keys = [] for expert_id in range(num_experts): expert_key = f"model.layers.{layer_id}.mlp.experts.{expert_id}.{proj_type}.weight" @@ -57,7 +67,7 @@ def moe_merge(state_dict: dict[str, torch.Tensor], config) -> dict[str, torch.Te processed_keys.update(current_expert_keys) for key in current_expert_keys: del state_dict[key] - print(f"✓ Layer {layer_id}.{proj_type}: {expert_weights[0].shape} -> {merged_weight.shape}") + print(f"✓ Layer {layer_id}.{proj_type}: {expert_weights[0].shape} -> {merged_weight.shape}") del expert_weights @@ -110,10 +120,21 @@ def split_moe_experts( def main(input_path, output_path): + # veomni is only needed to build the tokenizer and write out weights — import it lazily + # so this module (and its merge/split functions) can be imported without a VeOmni install. + from veomni.models import build_tokenizer, save_model_weights + torch.set_default_dtype(torch.bfloat16) os.makedirs(output_path, exist_ok=True) config = AutoConfig.from_pretrained(input_path, trust_remote_code=True) + + # Fail loudly on a non-LLaDA2 / inconsistent architecture; warn loudly on 2.2 block routing. + info = validate_llada2_config(config) + print(f"Detected LLaDA2 generation: {info['generation']} " + f"(num_experts={info['num_experts']}, block_routing={info['block_routing']})") + emit_warnings(info) + tokenizer = build_tokenizer(input_path) safetensor_files = list(glob(os.path.join(input_path, "*.safetensors"))) @@ -131,7 +152,7 @@ def main(input_path, output_path): new_state_dict = split_moe_experts(state_dict, config) else: raise ValueError("unsupport mode") - + state_dict.clear() model_assets = [config, tokenizer] save_model_weights(output_path, new_state_dict, model_assets=model_assets) diff --git a/tasks/train_llada2_common.py b/tasks/train_llada2_common.py index f79936a..68555dd 100644 --- a/tasks/train_llada2_common.py +++ b/tasks/train_llada2_common.py @@ -214,6 +214,18 @@ def _build_block_diffusion_mask(args: LLaDA2Arguments) -> Optional[torch.Tensor] return None full_len = args.data.max_seq_len * 2 + # This dense mask is (2*max_seq_len)^2 and is materialized on host before training. + # It is an O(L^2) memory footgun at long context: ~1 GiB at 8k, ~16 GiB at 32k, ~256 GiB + # at 128k. Block-diffusion is therefore unusable for long-context training — use + # block_diffusion_mode=false (as configs/longctx/* do). Warn loudly past ~2 GiB. + est_bytes = full_len * full_len * 4 # float32 upper bound + if est_bytes > 2 * 1024**3: + logger.info_rank0( + f"[WARNING] block_diffusion_mode with max_seq_len={args.data.max_seq_len} materializes " + f"a dense {full_len}x{full_len} attention mask (~{est_bytes / 1024**3:.1f} GiB). This " + "does not scale to long context — set block_diffusion_mode=false for long sequences. " + "See docs/UNIMPLEMENTED.md / MIGRATION_NOTES.md (Phase 4b)." + ) mask_flag = block_diffusion_mask( b=None, h=None, From 540f4611a5f4e8520274812935c378f9c5b05315 Mon Sep 17 00:00:00 2001 From: Akij Date: Fri, 24 Jul 2026 01:11:30 +0800 Subject: [PATCH 07/11] Add tiered test suite + CI; verify merge/split losslessness (Phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 1 (VeOmni-independent, runs on CPU with only torch+pytest — RAN HERE, 11 passed): - test_moe_convertor_roundtrip.py: synthetic llada2_moe separate-expert state dict -> moe_merge -> split_moe_experts is lossless (exact torch.equal); non-expert tensors pass through; corrupted expert dim rejected. This is the brief's required gate. - test_compat_validation.py: validate_llada2_config accepts 2.0/2.1/2.2 shapes, flags 2.2 block routing, rejects non-LLaDA2/inconsistent configs. Tier 2 (test_smoke_training.py): tiny eager-MoE model, one step per objective, checkpoint round-trip; importorskip('veomni') so it's the per-bump gate on a real stack (skips here). Tier 3 (test_integration_real_checkpoints.py): real-checkpoint merge/split, opt-in via LLADA2_INTEGRATION=1 + LLADA2_CKPT (multi-GB/GPU; skipped by default). Supporting change: moe_convertor.py now imports safetensors/transformers/tqdm/veomni lazily so merge/split are importable with only torch (enables Tier 1). CI: .github/workflows/tests.yml runs Tier 1 on Python 3.12 + CPU torch. Verified: pytest tests/ -> 11 passed, 2 skipped on py3.14 + torch 2.13.0+cpu. --- .github/workflows/tests.yml | 55 +++++++++ MIGRATION_NOTES.md | 10 ++ scripts/moe_convertor.py | 12 +- tests/README.md | 43 +++++++ tests/_synthetic.py | 127 +++++++++++++++++++++ tests/conftest.py | 9 ++ tests/test_compat_validation.py | 49 ++++++++ tests/test_integration_real_checkpoints.py | 58 ++++++++++ tests/test_moe_convertor_roundtrip.py | 74 ++++++++++++ tests/test_smoke_training.py | 79 +++++++++++++ 10 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/README.md create mode 100644 tests/_synthetic.py create mode 100644 tests/conftest.py create mode 100644 tests/test_compat_validation.py create mode 100644 tests/test_integration_real_checkpoints.py create mode 100644 tests/test_moe_convertor_roundtrip.py create mode 100644 tests/test_smoke_training.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..53d9c19 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: Tests + +on: + push: + paths: + - "scripts/**" + - "models/**" + - "tasks/**" + - "tests/**" + - ".github/workflows/tests.yml" + pull_request: + paths: + - "scripts/**" + - "models/**" + - "tasks/**" + - "tests/**" + - ".github/workflows/tests.yml" + workflow_dispatch: + +jobs: + cpu-tests: + name: "CPU tests (VeOmni-independent)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + # tests do NOT need the VeOmni submodule (they exercise the veomni-independent + # merge/split + validation logic), so we skip the (large) submodule checkout. + submodules: false + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" # within VeOmni's supported range (>=3.11,<3.13) + + - name: Install test deps (CPU torch only) + run: | + python -m pip install --upgrade pip + pip install pytest pyyaml + pip install torch --index-url https://download.pytorch.org/whl/cpu + + - name: Run CPU test suite + run: | + # The VeOmni smoke test and the real-checkpoint integration test skip automatically + # (no veomni installed; LLADA2_INTEGRATION unset). What runs here is the merge/split + # round-trip losslessness gate + config validation. + pytest tests/ -v + + - name: Syntax-check changed Python + run: | + python -m py_compile \ + scripts/moe_convertor.py \ + models/llada2_moe/compat.py \ + models/llada2_moe/configuration_llada2_moe.py \ + models/llada2_moe/editing.py diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md index 05a1dd4..1246e04 100644 --- a/MIGRATION_NOTES.md +++ b/MIGRATION_NOTES.md @@ -170,3 +170,13 @@ with CPU torch and will be. Everything else is static-only and labelled 🟡. - Consequence stated bluntly: SFT'ing a 2.2 checkpoint here uses standard per-token top-k routing + plain masked/block-diffusion labels → trains **around** the editing/routing machinery and may degrade RL-trained editing behaviour. Block routing differs in the **forward pass**, so it affects training too, not just inference. - Added **`models/llada2_moe/editing.py`**: `EditLabelConstructor` / `RoutingStrategy` interface stubs whose placeholder implementations **raise `NotImplementedError`** (verified) — never silent no-ops, and **not imported by the training path**. A real edit-label constructor could later plug into the `run_llada2_training(transform_builder=…)` hook. - Did **not** implement any of these mechanisms (unpublished; explicitly out of scope per the brief). + +### Phase 5 — Testing ✅ (Tier 1 actually executed here; Tiers 2/3 skip as designed) +- Added a `tests/` suite (pytest) in three tiers, plus a CI workflow `.github/workflows/tests.yml`. +- **Tier 1 (VeOmni-independent, RAN HERE):** installed `torch 2.13.0+cpu` + `pytest 9.1.1` and executed `pytest tests/` → **11 passed, 2 skipped in 7.5s** on Python 3.14. + - `test_moe_convertor_roundtrip.py` — **the gate the brief asks for**: a tiny synthetic `llada2_moe`-shaped separate-expert state dict → `moe_merge` → `split_moe_experts` is **lossless** (exact `torch.equal`), non-expert tensors pass through untouched, corrupted expert dim is rejected, merge stacks over the expert dim. ✅ **verified.** + - `test_compat_validation.py` — validator accepts 2.0/2.1/2.2-shaped configs, flags 2.2 block routing, raises on 4 bad configs. ✅ **verified.** +- **Tier 2 (`test_smoke_training.py`, SKIPPED here):** builds a tiny `llada2_moe` model (eager MoE, single-process CPU — no torchrun), runs a step for the full-attention and block-diffusion objectives, and round-trips the checkpoint. `importorskip("veomni")` → skips in this env; **this is the per-VeOmni-bump gate** for a machine with the stack. Written but 🟡 not executed here. +- **Tier 3 (`test_integration_real_checkpoints.py`, SKIPPED by default):** real-checkpoint config validation + real-shard merge/split losslessness; gated behind `LLADA2_INTEGRATION=1` + `LLADA2_CKPT`; documented as multi-GB / multi-GPU. 🟡 +- To make Tier 1 possible, `scripts/moe_convertor.py` was refactored so `moe_merge`/`split_moe_experts` import with **only torch** (safetensors/transformers/tqdm/veomni all pushed to their use sites). CI (`tests.yml`) runs Tier 1 on Python 3.12 + CPU torch, skips Tiers 2/3. +- **Blocker restated:** the Phase 2 requirement to *gate each VeOmni bump on the smoke test* still cannot be satisfied in this environment (Tier 2 needs veomni, which needs Linux + Python 3.11/3.12 + CUDA/NPU). Tier 1 gives real regression coverage for the conversion logic; Tier 2 is ready for whoever has the hardware. diff --git a/scripts/moe_convertor.py b/scripts/moe_convertor.py index b4b4422..fe16eea 100644 --- a/scripts/moe_convertor.py +++ b/scripts/moe_convertor.py @@ -7,9 +7,6 @@ from typing import Generator import torch -from safetensors.torch import safe_open -from tqdm import tqdm -from transformers import AutoConfig # Load the dependency-free LLaDA2 validator directly by file path. This avoids importing # the `models.llada2_moe` package (whose __init__ pulls in torch+veomni for training-time @@ -29,6 +26,8 @@ class StateDictIterator: def __iter__(self) -> Generator[tuple[str, torch.Tensor], None, None]: if self.filepath.endswith(".safetensors"): + from safetensors.torch import safe_open + with safe_open(self.filepath, framework="pt", device="cpu") as f: for key in f.keys(): yield key, f.get_tensor(key) @@ -120,8 +119,11 @@ def split_moe_experts( def main(input_path, output_path): - # veomni is only needed to build the tokenizer and write out weights — import it lazily - # so this module (and its merge/split functions) can be imported without a VeOmni install. + # Heavy deps (transformers/tqdm/veomni) are imported lazily so this module — and its + # moe_merge / split_moe_experts functions — can be imported and unit-tested with only torch. + from tqdm import tqdm + from transformers import AutoConfig + from veomni.models import build_tokenizer, save_model_weights torch.set_default_dtype(torch.bfloat16) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..a0b2188 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,43 @@ +# Tests + +Two tiers, so the important correctness check runs anywhere while the heavy paths stay opt-in. + +## Tier 1 — runnable on CPU, no VeOmni (this is what CI runs) + +Only needs `torch` (CPU) + `pytest`: + +```bash +pip install pytest torch --index-url https://download.pytorch.org/whl/cpu +pytest tests/ -v +``` + +- `test_moe_convertor_roundtrip.py` — **the gate**: builds a tiny synthetic separate-expert + state dict in the `llada2_moe` shape and asserts `moe_merge` → `split_moe_experts` is + **lossless** (exact tensor equality), non-expert tensors pass through untouched, and a + corrupted expert dim is rejected. +- `test_compat_validation.py` — `validate_llada2_config` accepts real 2.0/2.1/2.2-shaped + configs, flags 2.2 block routing, and raises on non-LLaDA2 / inconsistent configs. (No torch.) + +The VeOmni smoke test and the integration test **skip automatically** in this tier. + +## Tier 2 — full stack (VeOmni installed) + +`test_smoke_training.py` builds a tiny `llada2_moe` model (eager MoE, CPU-friendly, single +process — no torchrun), runs a step for the full-attention and block-diffusion objectives, and +round-trips its checkpoint. It `importorskip`s `veomni`, so it only runs where the training +stack is installed (Linux + Python 3.11/3.12 + the deps in `../requirements.txt`). + +**This is the per-VeOmni-bump gate**: after advancing the `VeOmni` submodule, run `pytest tests/` +on a machine with the stack and confirm Tier 1 + Tier 2 pass before committing the bump. + +## Tier 3 — real checkpoints (multi-GPU, manual) + +`test_integration_real_checkpoints.py` is skipped unless you opt in. It validates a real +checkpoint's config and asserts merge/split losslessness on its actual shards: + +```bash +LLADA2_INTEGRATION=1 LLADA2_CKPT=/path/to/LLaDA2.1-mini \ + pytest tests/test_integration_real_checkpoints.py -v +``` + +The 16B / 100B models need multi-GB weights (and multi-GPU for a real training run); not run in CI. diff --git a/tests/_synthetic.py b/tests/_synthetic.py new file mode 100644 index 0000000..f532c60 --- /dev/null +++ b/tests/_synthetic.py @@ -0,0 +1,127 @@ +"""Helpers to build a tiny synthetic LLaDA2-MoE-shaped model for tests. + +Deliberately small (few layers, tiny hidden, few experts, small vocab) so the whole thing +runs on CPU in milliseconds. Two flavours: + +- :func:`tiny_config` / :func:`make_separate_expert_state_dict` — a plain namespace + a + separate-expert state dict, enough to exercise ``moe_merge`` / ``split_moe_experts`` with + only ``torch`` installed (no transformers / veomni). +- :func:`tiny_config_json` — a real ``config.json`` dict in the ``llada2_moe`` shape for the + VeOmni-dependent smoke test. +""" +from __future__ import annotations + +import importlib.util +import os +from types import SimpleNamespace +from typing import Dict + + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def load_module_by_path(name: str, relpath: str): + """Import a module by file path without triggering package __init__ side effects.""" + path = os.path.join(REPO_ROOT, relpath) + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None, f"cannot load {path}" + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def tiny_config(**overrides) -> SimpleNamespace: + """A minimal config carrying just the fields moe_merge / split_moe_experts read.""" + cfg = dict( + model_type="llada2_moe_veomni", + architectures=["LLaDA2MoeModelLM"], + vocab_size=64, + hidden_size=16, + intermediate_size=32, + moe_intermediate_size=8, + num_hidden_layers=3, + num_attention_heads=4, + num_key_value_heads=2, + num_experts=4, + num_experts_per_tok=2, + num_shared_experts=1, + first_k_dense_replace=1, + ) + cfg.update(overrides) + return SimpleNamespace(**cfg) + + +def make_separate_expert_state_dict(cfg) -> Dict[str, "object"]: + """Build a separate-expert state dict (the HF layout moe_merge consumes). + + Includes non-expert tensors (embeddings, norms, a dense first layer) to verify they + pass through merge/split untouched. + """ + import torch + + torch.manual_seed(0) + h, mi = cfg.hidden_size, cfg.moe_intermediate_size + sd: Dict[str, torch.Tensor] = {} + sd["model.word_embeddings.weight"] = torch.randn(cfg.vocab_size, h) + sd["model.norm.weight"] = torch.randn(h) + sd["lm_head.weight"] = torch.randn(cfg.vocab_size, h) + + proj_shapes = {"gate_proj": (mi, h), "up_proj": (mi, h), "down_proj": (h, mi)} + for layer in range(cfg.num_hidden_layers): + sd[f"model.layers.{layer}.input_layernorm.weight"] = torch.randn(h) + sd[f"model.layers.{layer}.post_attention_layernorm.weight"] = torch.randn(h) + if layer < cfg.first_k_dense_replace: + # dense MLP layer — NOT expert-merged + for proj, (o, i) in {"gate_proj": (cfg.intermediate_size, h), + "up_proj": (cfg.intermediate_size, h), + "down_proj": (h, cfg.intermediate_size)}.items(): + sd[f"model.layers.{layer}.mlp.{proj}.weight"] = torch.randn(o, i) + else: + for e in range(cfg.num_experts): + for proj, (o, i) in proj_shapes.items(): + sd[f"model.layers.{layer}.mlp.experts.{e}.{proj}.weight"] = torch.randn(o, i) + return sd + + +def tiny_config_json(**overrides) -> dict: + """A real config.json dict in llada2_moe shape (for the VeOmni smoke test).""" + cfg = { + "architectures": ["LLaDA2MoeModelLM"], + "auto_map": { + "AutoConfig": "configuration_llada2_moe.LLaDA2MoeConfig", + "AutoModel": "modeling_llada2_moe.LLaDA2MoeModel", + "AutoModelForCausalLM": "modeling_llada2_moe.LLaDA2MoeModelLM", + }, + "model_type": "llada2_moe_veomni", + "vocab_size": 64, + "hidden_size": 16, + "intermediate_size": 32, + "moe_intermediate_size": 8, + "num_hidden_layers": 3, + "num_attention_heads": 4, + "num_key_value_heads": 2, + "head_dim": 4, + "num_experts": 4, + "num_experts_per_tok": 2, + "num_shared_experts": 1, + "n_group": 2, + "topk_group": 1, + "first_k_dense_replace": 1, + "hidden_act": "silu", + "rms_norm_eps": 1e-6, + "rope_theta": 10000.0, + "partial_rotary_factor": 0.5, + "max_position_embeddings": 128, + "pad_token_id": 0, + "use_bias": False, + "use_qkv_bias": False, + "use_qk_norm": True, + "tie_word_embeddings": False, + "routed_scaling_factor": 2.5, + "score_function": "sigmoid", + "moe_router_enable_expert_bias": True, + "norm_topk_prob": True, + "torch_dtype": "float32", + } + cfg.update(overrides) + return cfg diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d07f44a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +"""Make ``tests/`` and the repo root importable for the test modules.""" +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_ROOT = os.path.dirname(_HERE) +for p in (_HERE, _ROOT): + if p not in sys.path: + sys.path.insert(0, p) diff --git a/tests/test_compat_validation.py b/tests/test_compat_validation.py new file mode 100644 index 0000000..ee722bb --- /dev/null +++ b/tests/test_compat_validation.py @@ -0,0 +1,49 @@ +"""Phase 5: LLaDA2 config validation. Dependency-free — runs anywhere (no torch/transformers).""" +import pytest + +from _synthetic import load_module_by_path, tiny_config_json + +_compat = load_module_by_path("llada2_compat_under_test", "models/llada2_moe/compat.py") +validate = _compat.validate_llada2_config + + +def test_accepts_valid_20_21_config(): + info = validate(tiny_config_json()) + assert info["generation"] == "2.0/2.1" + assert info["block_routing"] is False + assert info["num_experts"] == 4 + assert info["warnings"] == [] + + +def test_detects_22_block_routing_and_warns(): + info = validate(tiny_config_json(expert_capacity=2, block_size=4, + max_position_embeddings=131072)) + assert info["generation"] == "2.2" + assert info["block_routing"] is True + # must warn loudly about unimplemented block routing AND long context + joined = " ".join(info["warnings"]).lower() + assert "block routing" in joined + assert "131072" in joined or "context" in joined + + +@pytest.mark.parametrize("bad", [ + {"model_type": "llama", "architectures": ["LlamaForCausalLM"], + "num_hidden_layers": 4, "num_experts": 8, "moe_intermediate_size": 8, + "hidden_size": 8, "num_experts_per_tok": 2}, # not llada2 + {"model_type": "llada2_moe", "num_hidden_layers": 2, "num_experts": 8, + "moe_intermediate_size": 8, "hidden_size": 8, "num_experts_per_tok": 2, + "first_k_dense_replace": 2}, # first_k >= layers + {"model_type": "llada2_moe", "num_hidden_layers": 4, "num_experts": 0, + "moe_intermediate_size": 8, "hidden_size": 8, "num_experts_per_tok": 2}, # zero experts + {"model_type": "llada2_moe"}, # missing fields +]) +def test_rejects_bad_configs(bad): + with pytest.raises(ValueError): + validate(bad) + + +def test_accepts_by_architecture_even_if_model_type_aliased(): + cfg = tiny_config_json() + cfg["model_type"] = "something_else" # but architectures still LLaDA2Moe* + info = validate(cfg) + assert info["generation"] == "2.0/2.1" diff --git a/tests/test_integration_real_checkpoints.py b/tests/test_integration_real_checkpoints.py new file mode 100644 index 0000000..efebc63 --- /dev/null +++ b/tests/test_integration_real_checkpoints.py @@ -0,0 +1,58 @@ +"""Phase 5 integration test against REAL LLaDA2 checkpoints. + +SKIPPED BY DEFAULT. This needs multi-GB weights and (for the 16B/100B models) multi-GPU. Enable +explicitly: + + LLADA2_INTEGRATION=1 LLADA2_CKPT=/path/to/LLaDA2.x-mini pytest tests/test_integration_real_checkpoints.py + +What it checks, given a real *separate-expert* checkpoint dir: + - AutoConfig loads and passes validate_llada2_config, + - moe_merge -> split_moe_experts on the real shards is lossless. + +Not run in CI. Documented as requiring a machine with the full VeOmni stack + GPU(s). +""" +import glob +import os + +import pytest + +RUN = os.environ.get("LLADA2_INTEGRATION") == "1" +CKPT = os.environ.get("LLADA2_CKPT", "") + +pytestmark = pytest.mark.skipif( + not RUN, reason="integration test; set LLADA2_INTEGRATION=1 and LLADA2_CKPT= to run" +) + + +def test_real_checkpoint_config_valid_and_merge_split_lossless(): + torch = pytest.importorskip("torch") + pytest.importorskip("safetensors") + transformers = pytest.importorskip("transformers") + + from _synthetic import load_module_by_path + + assert CKPT and os.path.isdir(CKPT), f"LLADA2_CKPT not a directory: {CKPT!r}" + + compat = load_module_by_path("llada2_compat_it", "models/llada2_moe/compat.py") + mc = load_module_by_path("moe_convertor_it", "scripts/moe_convertor.py") + + config = transformers.AutoConfig.from_pretrained(CKPT, trust_remote_code=True) + info = compat.validate_llada2_config(config) + assert info["num_experts"] > 0 + + from safetensors.torch import safe_open + + sd = {} + for shard in sorted(glob.glob(os.path.join(CKPT, "*.safetensors"))): + with safe_open(shard, framework="pt", device="cpu") as f: + for k in f.keys(): + sd[k] = f.get_tensor(k) + assert sd, "no safetensors shards found" + + reference = {k: v.clone() for k, v in sd.items()} + merged = mc.moe_merge(dict(sd), config) + restored = mc.split_moe_experts(merged, config) + + assert set(restored) == set(reference) + for k, v in reference.items(): + assert torch.equal(restored[k], v), f"round-trip mismatch: {k}" diff --git a/tests/test_moe_convertor_roundtrip.py b/tests/test_moe_convertor_roundtrip.py new file mode 100644 index 0000000..13ec9c1 --- /dev/null +++ b/tests/test_moe_convertor_roundtrip.py @@ -0,0 +1,74 @@ +"""Core Phase 5 gate: expert merge -> split must be LOSSLESS. + +Runs on CPU with only ``torch`` installed (no transformers/veomni). This is the assertion the +task brief calls out: "asserting the weights survive merge/split losslessly." +""" +import pytest + +torch = pytest.importorskip("torch") + +from _synthetic import load_module_by_path, make_separate_expert_state_dict, tiny_config + +# Import moe_merge / split_moe_experts by file path (module top-level needs only torch). +_mc = load_module_by_path("moe_convertor_under_test", "scripts/moe_convertor.py") +moe_merge = _mc.moe_merge +split_moe_experts = _mc.split_moe_experts + + +def _expert_keys(cfg): + keys = [] + for layer in range(cfg.first_k_dense_replace, cfg.num_hidden_layers): + for e in range(cfg.num_experts): + for proj in ("gate_proj", "up_proj", "down_proj"): + keys.append(f"model.layers.{layer}.mlp.experts.{e}.{proj}.weight") + return keys + + +def test_merge_produces_stacked_expert_tensors(): + cfg = tiny_config() + sd = make_separate_expert_state_dict(cfg) + merged = moe_merge(dict(sd), cfg) + + for layer in range(cfg.first_k_dense_replace, cfg.num_hidden_layers): + for proj in ("gate_proj", "up_proj", "down_proj"): + key = f"model.layers.{layer}.mlp.experts.{proj}" + assert key in merged, f"missing merged key {key}" + assert merged[key].shape[0] == cfg.num_experts, f"{key} not stacked over experts" + # per-expert keys must be gone after merge + for k in _expert_keys(cfg): + assert k not in merged + + +def test_merge_then_split_is_lossless(): + cfg = tiny_config() + original = make_separate_expert_state_dict(cfg) + # Keep a reference copy of the original tensors (moe_merge mutates the dict it is given). + reference = {k: v.clone() for k, v in original.items()} + + merged = moe_merge(dict(original), cfg) + restored = split_moe_experts(merged, cfg) + + assert set(restored.keys()) == set(reference.keys()), "key set changed across round-trip" + for k, v in reference.items(): + assert torch.equal(restored[k], v), f"tensor mismatch after round-trip: {k}" + + +def test_nonexpert_tensors_pass_through_unchanged(): + cfg = tiny_config() + original = make_separate_expert_state_dict(cfg) + reference = {k: v.clone() for k, v in original.items()} + + merged = moe_merge(dict(original), cfg) + for k in ("model.word_embeddings.weight", "model.norm.weight", "lm_head.weight", + "model.layers.0.mlp.gate_proj.weight"): # layer 0 is dense (first_k_dense_replace=1) + assert torch.equal(merged[k], reference[k]), f"non-expert tensor altered: {k}" + + +def test_split_rejects_wrong_expert_dim(): + cfg = tiny_config() + merged = moe_merge(make_separate_expert_state_dict(cfg), cfg) + # Corrupt one merged tensor's expert dimension. + bad_key = f"model.layers.{cfg.first_k_dense_replace}.mlp.experts.gate_proj" + merged[bad_key] = merged[bad_key][:1] # first dim no longer == num_experts + with pytest.raises(ValueError): + split_moe_experts(merged, cfg) diff --git a/tests/test_smoke_training.py b/tests/test_smoke_training.py new file mode 100644 index 0000000..b8c0488 --- /dev/null +++ b/tests/test_smoke_training.py @@ -0,0 +1,79 @@ +"""Phase 5 smoke test: build a tiny llada2_moe model, run a few training steps for each +diffusion objective, and round-trip its checkpoint. + +Requires the full stack (torch + transformers + veomni). SKIPPED automatically when veomni is +not importable — which is the case in the modernization authoring environment (Windows/py3.14). +It uses the EAGER MoE path (model_type='llada2_moe') so it runs single-process on CPU; no +torchrun / GPU / distributed init required. + +This is the per-bump gate the brief asks for: whoever advances the VeOmni submodule runs +`pytest tests/` on a machine with the stack installed and confirms this passes. +""" +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("transformers") +pytest.importorskip("veomni") # skips here; the model's modeling file imports veomni.ops + +from _synthetic import tiny_config_json # noqa: E402 + + +def _build_model(): + # Eager MoE path (CPU-friendly): model_type llada2_moe -> nn.ModuleList experts + _forward. + from models.llada2_moe.configuration_llada2_moe import LLaDA2MoeConfig + from models.llada2_moe.modeling_llada2_moe import LLaDA2MoeModelLM + + cfg_dict = tiny_config_json(model_type="llada2_moe") + config = LLaDA2MoeConfig(**{k: v for k, v in cfg_dict.items() + if k not in ("architectures", "auto_map")}) + torch.manual_seed(0) + model = LLaDA2MoeModelLM(config).to(torch.float32) + model.train() + return model, config + + +def _random_batch(config, seq_len=8, bsz=2): + ids = torch.randint(0, config.vocab_size, (bsz, seq_len)) + labels = ids.clone() + return ids, labels + + +def test_full_attention_objective_step(): + model, config = _build_model() + ids, labels = _random_batch(config) + out = model(input_ids=ids, attention_mask=None, use_cache=False) + logits = out.logits + loss = torch.nn.functional.cross_entropy( + logits[:, :-1].reshape(-1, config.vocab_size), labels[:, 1:].reshape(-1) + ) + assert torch.isfinite(loss), "non-finite loss (full-attention objective)" + loss.backward() + assert any(p.grad is not None and torch.isfinite(p.grad).all() + for p in model.parameters() if p.requires_grad) + + +def test_block_diffusion_shapes_and_step(): + model, config = _build_model() + seq_len = 8 + ids, labels = _random_batch(config, seq_len=seq_len) + # block-diffusion concatenates [noisy, clean] -> length 2*seq_len; feed via a 4D mask=None here + full_ids = torch.cat([ids, ids], dim=1) + out = model(input_ids=full_ids, attention_mask=None, use_cache=False) + assert out.logits.shape[1] == 2 * seq_len + noisy_logits = out.logits[:, :seq_len].contiguous() + loss = torch.nn.functional.cross_entropy( + noisy_logits.reshape(-1, config.vocab_size), labels.reshape(-1) + ) + assert torch.isfinite(loss) + loss.backward() + + +def test_checkpoint_state_dict_roundtrip(tmp_path): + model, _ = _build_model() + ref = {k: v.clone() for k, v in model.state_dict().items()} + path = tmp_path / "ckpt.pt" + torch.save(model.state_dict(), path) + reloaded = torch.load(path, map_location="cpu", weights_only=True) + assert set(reloaded) == set(ref) + for k, v in ref.items(): + assert torch.equal(reloaded[k], v), f"checkpoint tensor mismatch: {k}" From b1429b42d062a82edeabf7ac1cd824ece99a954b Mon Sep 17 00:00:00 2001 From: Akij Date: Fri, 24 Jul 2026 01:17:01 +0800 Subject: [PATCH 08/11] Rewrite README to reality; finalize MIGRATION_NOTES; fix launch cmds (Phase 6) - README: per-model support table with confidence levels (2.0/2.1 High/Medium; 2.2 low-fidelity + UNIMPLEMENTED pointer), verification-honesty callout, correct env (Py3.11-3.12, VeOmni 8ca09d7, torch 2.11+cu130, transformers 5.9.0), nested-config quickstart, positional-YAML launch, long-context + tests + known-limitations sections. - MIGRATION_NOTES: Phase 6 entry + consolidated verified/unverified/broken breakdown + branch map. - Fix launch command: config YAML is a positional arg to parse_args (nargs='?'), not --config; corrected in train_longctx.py and configs/longctx/*. Verified: pytest tests/ still 11 passed / 2 skipped; py_compile; YAML parses. --- MIGRATION_NOTES.md | 38 +++ README.md | 257 +++++++------------ configs/longctx/llada2_mini_longctx_64k.yaml | 2 +- tasks/train_longctx.py | 2 +- 4 files changed, 138 insertions(+), 161 deletions(-) diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md index 1246e04..ada734b 100644 --- a/MIGRATION_NOTES.md +++ b/MIGRATION_NOTES.md @@ -180,3 +180,41 @@ with CPU torch and will be. Everything else is static-only and labelled 🟡. - **Tier 3 (`test_integration_real_checkpoints.py`, SKIPPED by default):** real-checkpoint config validation + real-shard merge/split losslessness; gated behind `LLADA2_INTEGRATION=1` + `LLADA2_CKPT`; documented as multi-GB / multi-GPU. 🟡 - To make Tier 1 possible, `scripts/moe_convertor.py` was refactored so `moe_merge`/`split_moe_experts` import with **only torch** (safetensors/transformers/tqdm/veomni all pushed to their use sites). CI (`tests.yml`) runs Tier 1 on Python 3.12 + CPU torch, skips Tiers 2/3. - **Blocker restated:** the Phase 2 requirement to *gate each VeOmni bump on the smoke test* still cannot be satisfied in this environment (Tier 2 needs veomni, which needs Linux + Python 3.11/3.12 + CUDA/NPU). Tier 1 gives real regression coverage for the conversion logic; Tier 2 is ready for whoever has the hardware. + +### Phase 6 — Documentation & handoff ✅ +- **`README.md` rewritten** to reality: per-model support table with **confidence levels** (2.0/2.1 vs 2.2), explicit verification-honesty callout, correct environment (Python 3.11–3.12, VeOmni 8ca09d7, torch 2.11+cu130, transformers 5.9.0), nested-config quickstart, positional-YAML launch command, long-context section, tests section, and a Known-Limitations section pointing at `docs/UNIMPLEMENTED.md`. +- Fixed launch commands to use the **positional** config-file argument (VeOmni `parse_args` takes `config_file` as `nargs="?"`, not `--config`) in `train_longctx.py` and `configs/longctx/*`. +- Every phase is on its own `modernize/phaseN-*` branch with atomic commits (see below). + +--- + +## Final status — verified / unverified / broken + +**✅ Verified (actually executed in this environment):** +- Git baseline & upstream relationship; discovery of upstream PR #22 (Phase 0/1). +- MoE `merge → split` **losslessness** on a synthetic `llada2_moe` model (`pytest tests/` → 11 passed on torch 2.13.0+cpu). This is the central conversion-correctness claim. +- `validate_llada2_config` behaviour on **all four real downloaded configs** + negative cases. +- config.json diff across 2.0/2.1/2.1-flash/2.2 (isolated the exact 2.2 deltas). +- All configs/YAML parse; all changed Python passes `py_compile`; `extend_rope_context.py --help` runs. +- `editing.py` stubs raise `NotImplementedError`. + +**🟡 Unverified (written/ported, could NOT be run here — VeOmni needs Linux + Py3.11/3.12 + CUDA/NPU; this box is Windows + Py3.14 with no torch-GPU/veomni):** +- The VeOmni bump itself at runtime (adopted from PR #22, which *was* hardware-validated upstream on Ascend 910B2 — but not on this GPU). +- `train_longctx.py` + `configs/longctx/*` migration to the new API (my ports; structurally mirror the validated SFT path; **not** part of PR #22's validation). +- End-to-end SFT for any model; `AutoConfig.from_pretrained`/weight load/real-checkpoint conversion. +- The `requirements.txt` install on a supported interpreter. +- Tier 2 smoke test (`test_smoke_training.py`) and Tier 3 integration test. + +**❌ Broken / not done (by scope):** +- LLaDA2.2 **block routing, Levenshtein editing, L-EBPO** — documented in `docs/UNIMPLEMENTED.md`, interface stubs only (unpublished; out of scope). +- Per-VeOmni-bump smoke-gating (Phase 2's incremental-bisect ideal) — impossible without a runnable VeOmni here; the pin sits at PR #22's validated `8ca09d7` rather than being walked commit-by-commit. + +## Branch map (atomic, per phase) +- `modernize/phase1-upstream-sync` — baseline + sync findings (docs only). +- `modernize/phase2-veomni` — PR #22 merge (VeOmni 8ca09d7) + long-context port. +- `modernize/phase3-deps` — pinned `requirements.txt`. +- `modernize/phase4-model-support` — 2.1/2.2 config handling, validation, configs, UNIMPLEMENTED, stubs. +- `modernize/phase5-testing` — tiered test suite + CI. +- `modernize/phase6-docs` — README rewrite + notes finalize + launch-command fixes. + +Each branch chains off the previous. `.scratch_configs/` (downloaded HF config files) is gitignored and never committed. diff --git a/README.md b/README.md index bbbc679..60f7109 100644 --- a/README.md +++ b/README.md @@ -4,238 +4,177 @@

-[![License: MIT](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) -[![HuggingFace: Models](https://img.shields.io/badge/HuggingFace-Models-yellow)](https://huggingface.co/inclusionAI/LLaDA2.0-mini-preview) -[![Static Badge](https://img.shields.io/badge/Tutorial-Get_Start-red)](https://inclusionai.github.io/dFactory/) -[![Static Badge](https://img.shields.io/badge/DeepWiki-Explore-green)](https://deepwiki.com/inclusionAI/dFactory) - +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) +[![HuggingFace: Models](https://img.shields.io/badge/HuggingFace-Models-yellow)](https://huggingface.co/inclusionAI)

- # dFactory: Easy and Efficient dLLM Fine-Tuning -## Features - -- **Various models:** LLaDA2.0-mini (16B), LLaDA2.0-flash (100B) -- **Integrated methods:** (Continous) supervised-finetuning (block-diffusion, full attention), etc. +Fine-tuning framework for the **LLaDA2** family of MoE diffusion language models, built on +[VeOmni](https://github.com/ByteDance-Seed/VeOmni) (FSDP2, expert/sequence/context parallelism, +activation checkpointing). +> **This is a modernized fork.** It is *not* the same as upstream `inclusionAI/dFactory`. See +> [`MIGRATION_NOTES.md`](./MIGRATION_NOTES.md) for exactly what changed, what is verified, and +> what is not. Read the **confidence levels** below before spending GPU hours. -## Supported Models +## Features -| Model ID | Description | Size | Config Path | Hugging Face Link | -| --- | --- | --- | --- | --- | -| `inclusionAI/LLaDA2.0-mini-preview` | Instruction-tuned model, ready for downstream applications. | 16B | `configs/model_configs/llada2_mini/` | [🤗 Model Card](https://huggingface.co/inclusionAI/LLaDA2.0-mini-preview) | -| `inclusionAI/LLaDA2.0-mini` | Instruction-tuned model, ready for downstream applications. | 16B | `configs/model_configs/llada2_mini/` | [🤗 Model Card](https://huggingface.co/inclusionAI/LLaDA2.0-mini) | -| `inclusionAI/LLaDA2.0-flash-preview` | Instruction-tuned model, ready for downstream applications. | 100B | `configs/model_configs/llada2_flash/` | [🤗 Model Card](https://huggingface.co/inclusionAI/LLaDA2.0-flash-preview) | -| `inclusionAI/LLaDA2.0-flash` | Instruction-tuned model, ready for downstream applications. | 100B | `configs/model_configs/llada2_flash/` | [🤗 Model Card](https://huggingface.co/inclusionAI/LLaDA2.0-flash) | +- **Models:** LLaDA2.0 / 2.1 (mini 16B, flash 100B) and LLaDA2.2-flash (100B, 128K context) — + see the support table for what "supported" means per model. +- **Objectives:** supervised fine-tuning with **block-diffusion** (block-causal attention) or + **full-attention random-mask** diffusion; optional trainable parallel decoding. +- **Long context:** RoPE/YaRN/LongRoPE context extension + streaming long-sequence data + (`configs/longctx/`, `scripts/extend_rope_context.py`). -## TODO +## Supported models & confidence -We are actively working on enhancing the project with new features and improvements. Our roadmap for the near future includes: +| Model | Size | Load + convert | SFT (this fork) | Confidence | Notes | +| --- | --- | --- | --- | --- | --- | +| `LLaDA2.0-mini` / `-flash` | 16B / 100B | ✅ | ✅ | **High (upstream-validated)** | Base path. PR-#22 migration validated on Ascend NPU (parity vs legacy = 0). | +| `LLaDA2.1-mini` / `-flash` | 16B / 100B | ✅ | ✅ | **Medium** | Architecture byte-identical to 2.0 (verified by config diff). Same training path; configs added. | +| `LLaDA2.2-flash` | 100B | ✅ (merge/split verified lossless) | ⚠️ **plain-diffusion only** | **Low for full fidelity** | Loads/converts fine, but dFactory does **not** implement 2.2's block routing, Levenshtein editing, or L-EBPO — see [`docs/UNIMPLEMENTED.md`](./docs/UNIMPLEMENTED.md). SFT trains *around* the editing machinery. | -- [☑️] **Comprehensive Documentation**: A full documentation site is underway, which will feature in-depth tutorials, API references, and best practices. -- [☑️] **Trainable Parallel Decoding**: Integration of support for trainable parallel decoding to enable more advanced use cases. +**Confidence key.** *High* = validated on real hardware (upstream). *Medium* = architecture-verified +here and structurally identical to a High path, but not re-run end-to-end. *Low for full fidelity* += loads and trains, but a documented capability gap means results won't match the released model's +editing/routing behaviour. -Stay tuned for these updates! +> **Verification honesty.** This fork was assembled in an environment where the full training stack +> (VeOmni needs Linux + Python 3.11/3.12 + CUDA/NPU + flash-attn) could **not** run. What *was* +> executed here: the MoE merge/split losslessness test, config validation, config/YAML parsing, and +> `py_compile`. Everything requiring a live VeOmni/GPU is labelled unverified in `MIGRATION_NOTES.md`. -## Getting Started +## Environment -### 0. Environment Setup +- **Python 3.11–3.12** (VeOmni `requires-python >=3.11,<3.13`). Linux + CUDA 13 (or Ascend NPU). + Windows is not supported for training (no flash-attn wheels; VeOmni is Linux-first). +- Pinned **VeOmni** submodule: `8ca09d7` (2026-06). **torch 2.11.0+cu130**, **transformers 5.9.0**. + Full pins in [`requirements.txt`](./requirements.txt). -#### Option A: Use uv (Recommended) +### Install (recommended: uv, via VeOmni's extras) ```bash -# Install uv if not already installed -curl -LsSf https://astral.sh/uv/install.sh | sh - -git clone https://github.com/inclusionAI/dFactory.git --recursive +git clone dFactory --recursive cd dFactory/VeOmni - -# Install dependencies -uv sync --extra gpu - -# Activate environment +uv sync --extra gpu # resolves torch 2.11.0+cu130, transformers 5.9.0, flash-attn, ... source .venv/bin/activate - -# Back to our workdir cd .. ``` -#### Option B: Use pip +### Install (pip) ```bash -git clone https://github.com/inclusionAI/dFactory.git --recursive +git clone dFactory --recursive cd dFactory -pip install -e VeOmni/ +pip install -e VeOmni/ # installs the `veomni` package +pip install -r requirements.txt # transformers 5.9.0 etc. (see file header for torch index) ``` -### 1. Download and Merge Model Weights +If you cloned without `--recursive`: `git submodule update --init VeOmni`. -Our training scripts require model weights in a "merged-expert" format for optimal performance. Before starting, you must download the standard weights and convert them. +## Quickstart (LLaDA2.0-mini) -**1. Download the original model:** We provide a helper script to download the weights from the Hugging Face Hub. +**1. Download the model** ```bash -# Choose a destination for the original model files -python ./scripts/download_hf_model.py \ - --repo_id inclusionAI/LLaDA2.0-mini-preview \ - --local_dir /path/to/separate_expert_model +python ./scripts/download_hf_model.py --repo_id inclusionAI/LLaDA2.0-mini --local_dir ./LLaDA2.0-mini ``` -**2. Convert to the merged format:** Run the following script to create the merged checkpoint required for training. +**2. Merge experts** (training uses a merged-expert layout for batched MoE matmuls). The converter +now **validates the architecture** and warns loudly on a 2.2 (block-routing) checkpoint: ```bash -# Use the path from the previous step as the source -python scripts/moe_convertor.py \ - --input-path /path/to/separate_expert_model \ - --output-path /path/to/save/merged_model \ - --mode merge +python scripts/moe_convertor.py -i ./LLaDA2.0-mini -o ./LLaDA2.0-mini-moe-merge -m merge ``` -The directory `/path/to/save/merged_model` is what you will use for the training script. For more details, see [MoE Expert Merging and Splitting Utilities](#moe-expert-merging-and-splitting-utilities) - -### 2. Prepare Training Data - -Before training, the dataset must be prepared. This tutorial uses the `openai/gsm8k` dataset and demonstrates how to convert it into the conversational format. - -We provide an example script, `./scripts/build_gsm8k_dataset.py`, for this purpose. You can adapt this script or write your own to process other datasets. - -Running the following command executes the script. It converts the "question" and "answer" fields into a conversational messages field. The processed dataset is then saved to the ./gsm8k_datasets/ directory, split into two separate files: `train.jsonl` for training and `test.jsonl` for evaluation. +**3. Prepare data** (example converts GSM8K to the conversational format): ```bash python ./scripts/build_gsm8k_dataset.py ``` -### 3. Modify Training Configs +**4. Edit the config** — configs use the **nested VeOmni schema**. In +`configs/sft/llada2_mini_bd_sft.yaml`: -Edit `configs/sft/llada2_mini_bd_sft.yaml`: ```yaml model: - model_path: "/your/model/path" + model_path: ./LLaDA2.0-mini-moe-merge + tokenizer_path: ./LLaDA2.0-mini-moe-merge data: - train_path: "/your/data/path" + train_path: ./gsm8k_datasets/gsm8k_train.jsonl train: - output_dir: "/your/output/path" + checkpoint: + output_dir: ./llada2_mini_bd_sft_outputs ``` -### 4. Run Training - -With all preparations complete, you can now start the fine-tuning process with a single command: +**5. Train** (the YAML is a **positional** argument to the task script): ```bash -PYTHONPATH=$(pwd)/VeOmni:$PYTHONPATH sh train.sh tasks/train_llada2_bd.py configs/sft/llada2_mini_bd_sft.yaml +PYTHONPATH=$(pwd)/VeOmni:$(pwd)/tasks:$PYTHONPATH \ + sh train.sh tasks/train_llada2_bd.py configs/sft/llada2_mini_bd_sft.yaml ``` -### 5. Interacting with the Fine-Tuned Model - -To interact with your fine-tuned model, you must complete two main steps: converting the checkpoint and copying the modeling file. - -**Step 1: Convert the Checkpoint** +Config presets: `configs/sft/llada2_mini_bd_sft.yaml`, `llada2_flash_bd_sft.yaml`, +`llada2_1_mini_bd_sft.yaml`, `llada2_1_flash_bd_sft.yaml`, `llada2_2_flash_bd_sft.yaml` +(+ `*_npu.yaml` for Ascend). -First, you need to convert the checkpoint from the merged format used during training back to the standard Mixture-of-Experts (MoE) structure. - -> **Important: Finding the Correct Input Path** -> -> The --input-path for the conversion script is the path to the saved Hugging Face checkpoint, not the root output directory you specified during training. The checkpoint is typically located in a subdirectory like: -> -> TRAIN_OUTPUT_DIR/checkpoints/global_step_XXX/hf_ckpt/ - -Run the following command to perform the conversion: +**6. Convert back & chat** — split experts back to the HF layout, then copy the modeling file from +your *original* download: ```bash -python scripts/moe_convertor.py \ - --input-path /path/to/merged_model \ - --output-path /path/to/save/separate_expert_model \ - --mode split +python scripts/moe_convertor.py -i TRAIN_OUTPUT_DIR/checkpoints/global_step_XXX/hf_ckpt \ + -o ./LLaDA2.0-mini-finetuned -m split +cp ./LLaDA2.0-mini/modeling_llada2_moe.py ./LLaDA2.0-mini-finetuned/ ``` -**Step 2: Copy the Modeling File** +Then follow the model card to chat. -After the conversion, a final manual step is required. You must copy the model's architecture file (e.g., `modeling_llada2_moe.py`) into the newly created separate_expert_model directory. +## Long-context fine-tuning -This file must come from the directory of your original base model — the one you started with before any merge or training operations. The training and conversion processes only update the model weights, not the architecture file, which is why the original version is needed. +`tasks/train_longctx.py` + `configs/longctx/*` extend the mini model to 64k / 128k / 256k via +RoPE scaling and stream long sequences from Nemotron. First patch the RoPE config, then train +(again, positional YAML): ```bash -# Example: Copying from the initial, pre-merge model directory -cp /path/to/original_base_model/modeling_llada2_moe.py /path/to/save/separate_expert_model/ -``` - -With the model converted and the modeling file in place, you are now ready to chat! Follow the instructions on the [official model card](https://huggingface.co/inclusionAI/LLaDA2.0-mini-preview#%F0%9F%A4%97-hugging-face-transformers) to start a conversation with your model. +python scripts/extend_rope_context.py --model_path ./configs/model_configs/llada2_mini \ + --output_path ./configs/model_configs/llada2_mini_64k --target_length 65536 --method yarn -## MoE Expert Merging and Splitting Utilities - -We provide a utility script, `./scripts/moe_convertor.py`, to convert MoE model weights between two formats: - -1. Separate-Expert Format: The default format used by frameworks like Hugging Face transformers, where each expert's weights are stored as individual tensors. -2. Merged-Expert Format: A consolidated format where weights for all experts in a layer are stacked into a single, higher-dimensional tensor. - -### Merging Experts - -Convert a model with separate expert weights into the consolidated "merged" format. By merging expert weights, we can leverage highly efficient batched matrix multiplication on GPUs, significantly speeding up computation. - -**How it Works:** - -The script iterates through each MoE layer and stacks the weights of all experts (e.g., gate_proj, up_proj, down_proj) into a single tensor. - -- Before Merging (Separate Experts): +PYTHONPATH=$(pwd)/VeOmni:$(pwd)/tasks:$PYTHONPATH \ + sh train.sh tasks/train_longctx.py configs/longctx/llada2_mini_longctx_64k.yaml +``` - ``` - model.layers.15.mlp.experts.0.gate_proj.weight (shape: [4096, 14336]) - model.layers.15.mlp.experts.1.gate_proj.weight (shape: [4096, 14336]) - ... (and so on for all 8 experts) - ``` +Long context must use `block_diffusion_mode: false` (the block-diffusion mask is O((2·L)²) and +does not scale — a runtime guard warns). LLaDA2.2-flash is natively 128K (no RoPE patch needed +up to 128K). See `MIGRATION_NOTES.md` §Phase 4b for the ceiling and its verification status. -- After Merging (Merged Experts): +## MoE expert merge / split (`scripts/moe_convertor.py`) - ``` - model.layers.15.mlp.experts.gate_proj.weight (shape: [8, 4096, 14336]) - ``` +Converts between the HF **separate-expert** layout (one tensor per expert) and the **merged** +layout (`[num_experts, …]` stacks) used for training. `merge` before training, `split` after. The +round-trip is **lossless** (unit-tested — see below). Usage: `-m merge` / `-m split` with +`-i`/`-o`. -**Usage:** +## Tests ```bash -python scripts/moe_convertor.py \ - --input-path /path/to/separate_expert_model \ - --output-path /path/to/save/merged_model \ - --mode merge +pip install pytest torch --index-url https://download.pytorch.org/whl/cpu +pytest tests/ -v ``` -### Splitting Experts - -This process performs the reverse operation: it takes a model in the "merged" format and splits the expert weights back into separate tensors for each expert. This conversion is useful for: +Tier 1 (CPU, no VeOmni) runs in CI and covers the merge/split losslessness gate + config +validation. Tier 2 (`test_smoke_training.py`) needs the VeOmni stack and is the per-bump gate. +Tier 3 hits real checkpoints (opt-in, multi-GPU). See [`tests/README.md`](./tests/README.md). -- Fine-tuning: Converting a merged model back to the standard format for fine-tuning with frameworks like Hugging Face transformers. -- Analysis: Inspecting or modifying the weights of individual experts. -- Compatibility: Ensuring the model can be loaded by tools that expect separate expert weights. +## Known limitations -**How it Works:** - -The script identifies the merged weight tensors and slices them along the expert dimension to create individual weight files for each expert. - -- Before Splitting (Merged Experts): - - ``` - model.layers.15.mlp.experts.gate_proj.weight (shape: [8, 4096, 14336]) - ``` - -- After Splitting (Separate Experts): - - ``` - model.layers.15.mlp.experts.0.gate_proj.weight (shape: [4096, 14336]) - model.layers.15.mlp.experts.1.gate_proj.weight (shape: [4096, 14336]) - ... (and so on for all 8 experts) - ``` - -**Usage:** - -```bash -python scripts/moe_convertor.py \ - --input-path /path/to/merged_model \ - --output-path /path/to/save/separate_expert_model \ - --mode split -``` +- **LLaDA2.2** block routing, Levenshtein DELETE/INSERT editing, and L-EBPO RL are **not + implemented** ([`docs/UNIMPLEMENTED.md`](./docs/UNIMPLEMENTED.md)). SFT on a 2.2 checkpoint uses + the standard per-token top-k objective and may degrade its RL-trained editing behaviour. +- No RL (this is an SFT framework). No re-verified end-to-end training run in this fork's authoring + environment — see `MIGRATION_NOTES.md` for the verified/unverified breakdown. ## License -This project is licensed under the Apache 2.0 license - see the LICENSE file for details. +Apache 2.0 — see [LICENSE](./LICENSE). diff --git a/configs/longctx/llada2_mini_longctx_64k.yaml b/configs/longctx/llada2_mini_longctx_64k.yaml index 4abde70..04aa64c 100644 --- a/configs/longctx/llada2_mini_longctx_64k.yaml +++ b/configs/longctx/llada2_mini_longctx_64k.yaml @@ -7,7 +7,7 @@ # --target_length 65536 --method yarn # Then launch: # PYTHONPATH=$(pwd)/VeOmni:$(pwd)/tasks:$PYTHONPATH \ -# sh train.sh tasks/train_longctx.py --config configs/longctx/llada2_mini_longctx_64k.yaml +# sh train.sh tasks/train_longctx.py configs/longctx/llada2_mini_longctx_64k.yaml model: config_path: ./configs/model_configs/llada2_mini_64k # patched by extend_rope_context.py diff --git a/tasks/train_longctx.py b/tasks/train_longctx.py index 2bcac9f..0c8dfb8 100644 --- a/tasks/train_longctx.py +++ b/tasks/train_longctx.py @@ -20,7 +20,7 @@ Launch: PYTHONPATH=$(pwd)/VeOmni:$(pwd)/tasks:$PYTHONPATH \\ - sh train.sh tasks/train_longctx.py --config configs/longctx/llada2_mini_longctx_64k.yaml + sh train.sh tasks/train_longctx.py configs/longctx/llada2_mini_longctx_64k.yaml """ from dataclasses import dataclass, field from functools import partial From 8decba35cc3aadfae57bffbaa1b41534434e73d5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 17:45:00 +0000 Subject: [PATCH 09/11] Fix: Log warning when micro-batch has no valid supervised positions The _compute_llada2_loss function used .clamp_min(1) on the masked-token count which silently masked the case when a micro-batch had no valid supervised positions (all labels == -100). This could hide data quality issues or bugs in the data pipeline. The fix separates the denominator calculation from the clamping and adds a warning log when the denominator is zero, surfacing the empty-supervision condition while maintaining numerical stability. --- tasks/train_llada2_common.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tasks/train_llada2_common.py b/tasks/train_llada2_common.py index 68555dd..d83b66b 100644 --- a/tasks/train_llada2_common.py +++ b/tasks/train_llada2_common.py @@ -288,8 +288,13 @@ def _compute_llada2_loss( labels.view(-1), reduction="none", ) - denom = (labels != -100).sum().clamp_min(1) - consistency_loss = unscaled_loss.sum() / denom + denom = (labels != -100).sum() + if denom == 0: + logger.warning( + "Micro-batch has no valid supervised positions (all labels are -100). " + "This may indicate bad data or a bug in the data pipeline." + ) + consistency_loss = unscaled_loss.sum() / denom.clamp_min(1) else: shifted_noisy_logits = noisy_logits[:, :-1, :].contiguous() shifted_labels = labels[:, 1:].contiguous() @@ -298,8 +303,13 @@ def _compute_llada2_loss( shifted_labels.view(-1), reduction="none", ) - denom = (shifted_labels != -100).sum().clamp_min(1) - consistency_loss = unscaled_loss.sum() / denom + denom = (shifted_labels != -100).sum() + if denom == 0: + logger.warning( + "Micro-batch has no valid supervised positions (all labels are -100). " + "This may indicate bad data or a bug in the data pipeline." + ) + consistency_loss = unscaled_loss.sum() / denom.clamp_min(1) combined_loss = consistency_loss + confidence_loss * args.train.confidence_beta return combined_loss / num_micro_steps, consistency_loss, confidence_loss From 737e658a922d6917dfd826bb44947934c72d94e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 17:48:42 +0000 Subject: [PATCH 10/11] Fix NameError in moe_convertor main() by passing mode parameter The main() function was referencing args.mode which was only defined in the __main__ block. Added mode as a parameter to main() and updated the call site to pass args.mode. --- scripts/moe_convertor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/moe_convertor.py b/scripts/moe_convertor.py index fe16eea..ae7e868 100644 --- a/scripts/moe_convertor.py +++ b/scripts/moe_convertor.py @@ -118,7 +118,7 @@ def split_moe_experts( return split_state_dict -def main(input_path, output_path): +def main(input_path, output_path, mode): # Heavy deps (transformers/tqdm/veomni) are imported lazily so this module — and its # moe_merge / split_moe_experts functions — can be imported and unit-tested with only torch. from tqdm import tqdm @@ -148,9 +148,9 @@ def main(input_path, output_path): for name, tensor in state_dict_iterator: state_dict[name] = tensor.cpu() - if args.mode == "merge": + if mode == "merge": new_state_dict = moe_merge(state_dict, config) - elif args.mode == "split": + elif mode == "split": new_state_dict = split_moe_experts(state_dict, config) else: raise ValueError("unsupport mode") @@ -166,4 +166,4 @@ def main(input_path, output_path): parser.add_argument("-o", "--output-path", type=str, required=True) parser.add_argument("-m", "--mode", type=str, default="merge", choices=["merge", "split"]) args = parser.parse_args() - main(args.input_path, args.output_path) + main(args.input_path, args.output_path, args.mode) From 16893b1aa8d20503b0cff4e633a632d3e8f06e5e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 17:55:39 +0000 Subject: [PATCH 11/11] Fix zero routing fields misclassification in validate_llada2_config The is_block_routing check now requires expert_capacity and block_size to be positive values, not just non-None. This prevents configs with zero values (malformed or placeholder) from being incorrectly classified as LLaDA2.2 block routing checkpoints. --- models/llada2_moe/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/llada2_moe/compat.py b/models/llada2_moe/compat.py index 0c9cfab..c711f53 100644 --- a/models/llada2_moe/compat.py +++ b/models/llada2_moe/compat.py @@ -87,7 +87,7 @@ def validate_llada2_config(config: Any) -> Dict[str, Any]: warnings: List[str] = [] expert_capacity = _get(config, "expert_capacity", None) block_size = _get(config, "block_size", None) - is_block_routing = expert_capacity is not None or block_size is not None + is_block_routing = (expert_capacity is not None and expert_capacity > 0) or (block_size is not None and block_size > 0) generation = "2.2" if is_block_routing else "2.0/2.1" if is_block_routing: