From 0fa0ea776a9f3268cf357542f153bffb1dcef13b Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Fri, 13 Mar 2026 17:31:59 +0800 Subject: [PATCH 01/23] dflash --- .gitignore | 2 +- angelslim/compressor/quant/ptq.py | 8 +- .../models/eagle3/target/modeling_qwen2_kv.py | 9 +- .../models/eagle3/target/modeling_qwen3_kv.py | 9 +- .../speculative/train/data/data_utils.py | 6 +- .../speculative/train/data/dataset.py | 37 +- .../dataset_builder/base_dataset_builder.py | 12 +- .../dataset_builder/online_dataset_builder.py | 40 +- .../train/models/draft/__init__.py | 2 + .../train/models/draft/qwen_dflash.py | 418 +++++++++++++ .../models/target/target_model_wrapper.py | 5 +- .../speculative/train/trainer/__init__.py | 4 + .../train/trainer/offline_dflash_trainer.py | 44 ++ .../train/trainer/online_dflash_trainer.py | 569 ++++++++++++++++++ angelslim/engine.py | 2 +- angelslim/models/llm/tiktoken_tokenizer.py | 21 +- angelslim/models/omni/qwen3_omni.py | 6 +- angelslim/models/vlm/qwen3_vl.py | 7 +- angelslim/models/vlm/qwen3_vl_moe.py | 9 +- configs/qwen3_dflash.json | 54 ++ scripts/speculative/generate_dflash_data.sh | 40 ++ scripts/speculative/run_dflash_offline.sh | 54 ++ scripts/speculative/run_dflash_online.sh | 54 ++ tools/generate_dflash_data.py | 291 +++++++++ tools/train_dflash_offline.py | 310 ++++++++++ tools/train_dflash_online.py | 524 ++++++++++++++++ 26 files changed, 2507 insertions(+), 30 deletions(-) create mode 100755 angelslim/compressor/speculative/train/models/draft/qwen_dflash.py create mode 100644 angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py create mode 100755 angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py create mode 100755 configs/qwen3_dflash.json create mode 100644 scripts/speculative/generate_dflash_data.sh create mode 100644 scripts/speculative/run_dflash_offline.sh create mode 100644 scripts/speculative/run_dflash_online.sh create mode 100755 tools/generate_dflash_data.py create mode 100755 tools/train_dflash_offline.py create mode 100755 tools/train_dflash_online.py diff --git a/.gitignore b/.gitignore index 7935db5e..393a1409 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ output/ outs/ wandb/ tools/results/ -__pycache__/ \ No newline at end of file +__pycache__/outputs/ diff --git a/angelslim/compressor/quant/ptq.py b/angelslim/compressor/quant/ptq.py index cca48b3d..02e5cc4a 100644 --- a/angelslim/compressor/quant/ptq.py +++ b/angelslim/compressor/quant/ptq.py @@ -18,7 +18,11 @@ import torch from safetensors.torch import load_file -from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts +try: + from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts +except ImportError: + Qwen3VLMoeTextExperts = None # not available in this transformers version + from ...utils import find_parent_layer_and_sub_name, print_info from ..compressor_factory import CompressorFactory @@ -282,7 +286,7 @@ def _convert(self): # For qwen3_vl_moe models, we need to insert MoEQDQModule for MOE experts, # since these modules contain gate_up_proj and down_proj, which are defined as # nn.Parameters, not nn.Linear. - if Qwen3VLMoeTextExperts in self.quant_model.observer_layer_classes: + if Qwen3VLMoeTextExperts is not None and Qwen3VLMoeTextExperts in self.quant_model.observer_layer_classes: for name, sub_layer in self.quant_model.model.named_modules(): parent_layer, sub_name = find_parent_layer_and_sub_name(quant_convert_module, name) moe_qdq_module = self.quant_model.get_moe_qdq_module(sub_layer, name) diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py index 3c2d5a71..1d7cf5bc 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py @@ -15,7 +15,14 @@ import torch from torch import nn from transformers.activations import ACT2FN -from transformers.cache_utils import Cache, SlidingWindowCache, StaticCache +from transformers.cache_utils import Cache, StaticCache +try: + from transformers.cache_utils import SlidingWindowCache +except ImportError: + # SlidingWindowCache was removed/renamed in newer transformers versions. + # Define a stub so isinstance() checks below still work (they'll just never match). + class SlidingWindowCache: # type: ignore[no-redef] + pass from transformers.generation import GenerationMixin from transformers.modeling_attn_mask_utils import AttentionMaskConverter from transformers.modeling_flash_attention_utils import FlashAttentionKwargs diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py index a617e84c..e3e41904 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py @@ -15,7 +15,14 @@ import torch from torch import nn from transformers.activations import ACT2FN -from transformers.cache_utils import Cache, SlidingWindowCache, StaticCache +from transformers.cache_utils import Cache, StaticCache +try: + from transformers.cache_utils import SlidingWindowCache +except ImportError: + # SlidingWindowCache was removed/renamed in newer transformers versions. + # Define a stub so isinstance() checks below still work (they'll just never match). + class SlidingWindowCache: # type: ignore[no-redef] + pass from transformers.generation import GenerationMixin from transformers.modeling_attn_mask_utils import AttentionMaskConverter from transformers.modeling_flash_attention_utils import FlashAttentionKwargs diff --git a/angelslim/compressor/speculative/train/data/data_utils.py b/angelslim/compressor/speculative/train/data/data_utils.py index ace2b186..7725af78 100644 --- a/angelslim/compressor/speculative/train/data/data_utils.py +++ b/angelslim/compressor/speculative/train/data/data_utils.py @@ -184,11 +184,13 @@ def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, Any]: "target_hiddens": None, } - # Check if both hidden_states and target_hiddens exist in all features - if all("hidden_states" in item and "target_hiddens" in item for item in features): + # Handle hidden_states and target_hiddens independently + if all("hidden_states" in item for item in features): batch["hidden_states"] = torch.cat( [paddingtensor(item["hidden_states"], max_length) for item in features] ) + + if all("target_hiddens" in item for item in features): batch["target_hiddens"] = torch.cat( [paddingtensor(item["target_hiddens"], max_length) for item in features] ) diff --git a/angelslim/compressor/speculative/train/data/dataset.py b/angelslim/compressor/speculative/train/data/dataset.py index 722f0d15..9735b327 100644 --- a/angelslim/compressor/speculative/train/data/dataset.py +++ b/angelslim/compressor/speculative/train/data/dataset.py @@ -174,41 +174,56 @@ def _create_online_datasets( if self.display: num_proc = None + # Determine min_loss_tokens for DFlash filtering + min_loss_tokens = None + if self.data_args.modal_type == "DFlash": + block_size = getattr(self.data_args, "block_size", 16) + min_loss_tokens = 2 * block_size + # Create training dataset train_dataset = None - if self.data_args.train_data_path is not None: + train_path = getattr(self.data_args, "train_data_path", None) + if train_path is not None: train_dataset = self.online_dataset_builder.build_dataset( - self.data_args.train_data_path, + train_path, num_proc=num_proc, shuffle=True, - sample_num=self.data_args.sample_num, + sample_num=getattr(self.data_args, "sample_num", None), + min_loss_tokens=min_loss_tokens, ) # Create evaluation dataset eval_dataset = None - if self.data_args.eval_data_path is not None: + eval_path = getattr(self.data_args, "eval_data_path", None) + if eval_path is not None: eval_dataset = self.online_dataset_builder.build_dataset( - self.data_args.eval_data_path, + eval_path, num_proc=num_proc, shuffle=False, - sample_num=self.data_args.sample_num, + sample_num=getattr(self.data_args, "sample_num", None), + min_loss_tokens=min_loss_tokens, ) data_collator = self.online_dataset_builder.get_data_collator() return train_dataset, eval_dataset, data_collator - def _create_offline_datasets(self) -> Tuple[Dataset, Optional[Dataset]]: + def _create_offline_datasets(self) -> Tuple[Dataset, Optional[Dataset], Any]: """ Create offline datasets from pre-computed .ckpt files. Returns: - Tuple of (train_dataset, eval_dataset) + Tuple of (train_dataset, eval_dataset, data_collator) """ + if self.offline_dataset_builder is None: + return None, None, None + # Create train dataset - train_dataset = self.offline_dataset_builder.build_dataset( - self.data_args.train_hidden_path - ) + train_dataset = None + if self.data_args.train_hidden_path is not None: + train_dataset = self.offline_dataset_builder.build_dataset( + self.data_args.train_hidden_path + ) # Create eval dataset if path is provided eval_dataset = None diff --git a/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py b/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py index 7259149c..72437a05 100644 --- a/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py +++ b/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py @@ -29,7 +29,7 @@ class DatasetBuilder(metaclass=ABCMeta): @abstractmethod def build_dataset( - self, datapath: str, num_proc: int = 8, shuffle: bool = True, **kwargs + self, datapath: str, num_proc: int = 8, shuffle: bool = True, min_loss_tokens: Optional[int] = None, **kwargs ) -> Dataset: pass @@ -127,6 +127,7 @@ def build_dataset( num_proc: int = 8, shuffle: bool = True, sample_num: Optional[int] = None, + min_loss_tokens: Optional[int] = None, ) -> Dataset: try: # Load dataset @@ -161,6 +162,15 @@ def build_dataset( num_proc=num_proc, desc="Filtering empty input_ids", ) + + if min_loss_tokens is not None: + processed_ds = processed_ds.filter( + lambda batch: [sum(sum(x) if isinstance(x, list) else x for x in m) >= min_loss_tokens for m in batch["loss_mask"]], + batched=True, + num_proc=num_proc, + desc=f"Filtering sequences with loss tokens < {min_loss_tokens}", + ) + processed_ds.set_format(type="torch") return processed_ds diff --git a/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py b/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py index 9a662eff..e09561e8 100644 --- a/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py +++ b/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py @@ -94,6 +94,7 @@ def build_dataset( num_proc: int = 8, shuffle: bool = True, sample_num: Optional[int] = None, + min_loss_tokens: Optional[int] = None, ) -> Dataset: try: # Load dataset @@ -146,11 +147,19 @@ def build_dataset( num_proc=num_proc, desc="Filtering empty input_ids", ) + if min_loss_tokens is not None: + processed_ds = processed_ds.filter( + lambda batch: [ + sum(sum(x) if isinstance(x, list) else x for x in m) >= min_loss_tokens + for m in batch["loss_mask"] + ], + batched=True, + num_proc=num_proc, + desc=f"Filtering sequences with loss tokens < {min_loss_tokens}", + ) + torch_columns = [c for c in processed_ds.column_names if c != "image_paths"] processed_ds.set_format(type="torch", columns=torch_columns, output_all_columns=True) - rank0_print( - f"processed_ds size:{len(processed_ds)}, columns: {processed_ds.column_names}" - ) return processed_ds @@ -324,6 +333,7 @@ def build_dataset( num_proc: int = 8, shuffle: bool = True, sample_num: Optional[int] = None, + min_loss_tokens: Optional[int] = None, ) -> Dataset: try: # Load dataset @@ -374,6 +384,16 @@ def build_dataset( num_proc=num_proc, desc="Filtering empty input_ids", ) + if min_loss_tokens is not None: + processed_ds = processed_ds.filter( + lambda batch: [ + sum(sum(x) if isinstance(x, list) else x for x in m) >= min_loss_tokens + for m in batch["loss_mask"] + ], + batched=True, + num_proc=num_proc, + desc=f"Filtering sequences with loss tokens < {min_loss_tokens}", + ) torch_columns = [c for c in processed_ds.column_names if c != "image_paths"] processed_ds.set_format(type="torch", columns=torch_columns, output_all_columns=True) @@ -572,6 +592,7 @@ def build_dataset( num_proc: int = 8, shuffle: bool = True, sample_num: Optional[int] = None, + min_loss_tokens: Optional[int] = None, ) -> Dataset: try: # Load dataset @@ -623,6 +644,18 @@ def build_dataset( num_proc=num_proc, desc="Filtering empty input_ids", ) + + if min_loss_tokens is not None: + processed_ds = processed_ds.filter( + lambda batch: [ + sum(sum(x) if isinstance(x, list) else x for x in m) >= min_loss_tokens + for m in batch["loss_mask"] + ], + batched=True, + num_proc=num_proc, + desc=f"Filtering sequences with loss tokens < {min_loss_tokens}", + ) + processed_ds.set_format(type="torch") return processed_ds @@ -886,6 +919,7 @@ def build_dataset( num_proc: int = 8, shuffle: bool = True, sample_num: Optional[int] = None, + min_loss_tokens: Optional[int] = None, ) -> Dataset: try: if not isinstance(datapath, list): diff --git a/angelslim/compressor/speculative/train/models/draft/__init__.py b/angelslim/compressor/speculative/train/models/draft/__init__.py index 1b1eb4b9..c056ce23 100644 --- a/angelslim/compressor/speculative/train/models/draft/__init__.py +++ b/angelslim/compressor/speculative/train/models/draft/__init__.py @@ -14,10 +14,12 @@ from .draft_model_factory import DraftModelConfig, create_draft_model from .llama_eagle3 import CosyVoice3Eagle3LlamaForCausalLM, Eagle3LlamaForCausalLM +from .qwen_dflash import QwenDFlashDraftModel __all__ = [ "create_draft_model", "DraftModelConfig", "Eagle3LlamaForCausalLM", "CosyVoice3Eagle3LlamaForCausalLM", + "QwenDFlashDraftModel", ] diff --git a/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py b/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py new file mode 100755 index 00000000..b33e1649 --- /dev/null +++ b/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py @@ -0,0 +1,418 @@ +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DFlash Draft Model for Qwen3 architecture. + +Migrated from SpecForge's specforge/modeling/draft/dflash.py. +Uses cross-attention between draft blocks and context hidden states, +fundamentally different from Eagle3's concat + self-attention approach. +""" + +from typing import Callable, List, Optional, Tuple + +import torch +from torch import nn +from transformers import DynamicCache +from transformers.cache_utils import Cache +from transformers.modeling_outputs import CausalLMOutputWithPast +from transformers.models.qwen3.modeling_qwen3 import ( + ALL_ATTENTION_FUNCTIONS, + FlashAttentionKwargs, + GradientCheckpointingLayer, + Qwen3Config, + Qwen3MLP, + Qwen3PreTrainedModel, + Qwen3RMSNorm, + Qwen3RotaryEmbedding, + eager_attention_forward, + rotate_half, +) +from typing_extensions import Unpack + +from .draft_model_factory import DraftModelFactory + + +def sample(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor: + if temperature < 1e-5: + return torch.argmax(logits, dim=-1) + bsz, seq_len, vocab_size = logits.shape + logits = logits.view(-1, vocab_size) + logits = logits / temperature + probs = torch.softmax(logits, dim=-1) + return torch.multinomial(probs, num_samples=1).view(bsz, seq_len) + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_len = q.size(-2) + q_embed = (q * cos[..., -q_len:, :]) + (rotate_half(q) * sin[..., -q_len:, :]) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + +def build_target_layer_ids( + num_target_layers: int, num_draft_layers: int +) -> List[int]: + """Compute target layer IDs to capture from the target model.""" + if num_draft_layers == 1: + return [(num_target_layers // 2)] + start = 1 + end = num_target_layers - 3 + span = end - start + target_layer_ids = [ + int(round(start + (i * span) / (num_draft_layers - 1))) + for i in range(num_draft_layers) + ] + return target_layer_ids + + +def extract_context_feature( + hidden_states: list, + layer_ids: Optional[List[int]], +) -> torch.Tensor: + """Extract and concatenate hidden states from specified layers.""" + offset = 1 + selected_states = [] + for layer_id in layer_ids: + selected_states.append(hidden_states[layer_id + offset]) + target_hidden = torch.cat(selected_states, dim=-1) + return target_hidden + + +class Qwen3DFlashAttention(nn.Module): + """Multi-headed cross-attention for DFlash. + + Q comes from draft hidden states, KV comes from concatenation of + context (target) hidden states and draft hidden states. + """ + + def __init__(self, config: Qwen3Config, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr( + config, "head_dim", config.hidden_size // config.num_attention_heads + ) + self.num_key_value_groups = ( + config.num_attention_heads // config.num_key_value_heads + ) + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = False + self.q_proj = nn.Linear( + config.hidden_size, + config.num_attention_heads * self.head_dim, + bias=config.attention_bias, + ) + self.k_proj = nn.Linear( + config.hidden_size, + config.num_key_value_heads * self.head_dim, + bias=config.attention_bias, + ) + self.v_proj = nn.Linear( + config.hidden_size, + config.num_key_value_heads * self.head_dim, + bias=config.attention_bias, + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, + config.hidden_size, + bias=config.attention_bias, + ) + self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) + self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) + self.sliding_window = ( + config.sliding_window + if config.layer_types[layer_idx] == "sliding_attention" + else None + ) + + def forward( + self, + hidden_states: torch.Tensor, + target_hidden: torch.Tensor, + position_embeddings: Tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + bsz, q_len = hidden_states.shape[:-1] + ctx_len = target_hidden.shape[1] + q = self.q_proj(hidden_states) + q = q.view(bsz, q_len, -1, self.head_dim) + q = self.q_norm(q).transpose(1, 2) + k_ctx = self.k_proj(target_hidden) + k_noise = self.k_proj(hidden_states) + v_ctx = self.v_proj(target_hidden) + v_noise = self.v_proj(hidden_states) + k = torch.cat([k_ctx, k_noise], dim=1).view( + bsz, ctx_len + q_len, -1, self.head_dim + ) + v = torch.cat([v_ctx, v_noise], dim=1).view( + bsz, ctx_len + q_len, -1, self.head_dim + ) + k = self.k_norm(k).transpose(1, 2) + v = v.transpose(1, 2) + cos, sin = position_embeddings + q, k = apply_rotary_pos_emb(q, k, cos, sin) + if past_key_values is not None: + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + k, v = past_key_values.update(k, v, self.layer_idx, cache_kwargs) + attn_fn: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + attn_output, attn_weights = attn_fn( + self, + q, + k, + v, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, + ) + attn_output = attn_output.reshape(bsz, q_len, -1) + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + +class Qwen3DFlashDecoderLayer(GradientCheckpointingLayer): + """DFlash decoder layer with cross-attention to context.""" + + def __init__(self, config: Qwen3Config, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + self.self_attn = Qwen3DFlashAttention(config=config, layer_idx=layer_idx) + self.mlp = Qwen3MLP(config) + self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen3RMSNorm( + config.hidden_size, eps=config.rms_norm_eps + ) + + def forward( + self, + target_hidden: Optional[torch.Tensor] = None, + hidden_states: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_value: Optional[Cache] = None, + output_attentions: Optional[bool] = False, + use_cache: Optional[bool] = False, + cache_position: Optional[torch.LongTensor] = None, + position_embeddings: Optional[ + Tuple[torch.Tensor, torch.Tensor] + ] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> torch.FloatTensor: + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + hidden_states = self.self_attn( + hidden_states=hidden_states, + target_hidden=target_hidden, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_value, + output_attentions=output_attentions, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + )[0] + hidden_states = residual + hidden_states + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + return hidden_states + + +@DraftModelFactory.register +class QwenDFlashDraftModel(Qwen3PreTrainedModel): + """DFlash Draft Model for Qwen3 architecture. + + Uses block-parallel cross-attention between noise-masked draft blocks + and context hidden states from the target model. + """ + + config_class = Qwen3Config + _no_split_modules = ["Qwen3DFlashDecoderLayer"] + + def __init__(self, config) -> None: + super().__init__(config) + self.config = config + self.layers = nn.ModuleList( + [ + Qwen3DFlashDecoderLayer(config, layer_idx) + for layer_idx in range(config.num_hidden_layers) + ] + ) + dflash_config = getattr(config, "dflash_config", {}) or {} + self.target_layer_ids = dflash_config.get( + "target_layer_ids", + build_target_layer_ids(config.num_target_layers, config.num_hidden_layers), + ) + self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3RotaryEmbedding(config) + self.fc = nn.Linear( + len(self.target_layer_ids) * config.hidden_size, + config.hidden_size, + bias=False, + ) + self.hidden_norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.block_size = config.block_size + self.mask_token_id = dflash_config.get("mask_token_id", None) + self.post_init() + + def forward( + self, + position_ids: torch.LongTensor, + attention_mask: Optional[torch.Tensor] = None, + noise_embedding: Optional[torch.Tensor] = None, + target_hidden: Optional[torch.Tensor] = None, + past_key_values: Optional[Cache] = None, + use_cache: bool = False, + **kwargs, + ) -> torch.Tensor: + hidden_states = noise_embedding + target_hidden = self.hidden_norm(self.fc(target_hidden)) + position_embeddings = self.rotary_emb(hidden_states, position_ids) + for layer in self.layers: + hidden_states = layer( + hidden_states=hidden_states, + target_hidden=target_hidden, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_value=past_key_values, + use_cache=use_cache, + position_embeddings=position_embeddings, + **kwargs, + ) + return self.norm(hidden_states) + + @torch.inference_mode() + def spec_generate( + self, + target: nn.Module, + input_ids: torch.LongTensor, + max_new_tokens: int, + stop_token_ids: List[int], + temperature: float, + ): + """Speculative generation with DFlash draft model.""" + self.eval() + num_input_tokens = input_ids.shape[1] + max_length = num_input_tokens + max_new_tokens + + block_size = self.block_size + output_ids = torch.full( + (1, max_length + block_size), + self.mask_token_id, + dtype=torch.long, + device=target.device, + ) + position_ids = torch.arange( + output_ids.shape[1], device=target.device + ).unsqueeze(0) + + past_key_values_target = DynamicCache() + past_key_values_draft = DynamicCache() + + # Prefill stage + output = target( + input_ids, + position_ids=position_ids[:, :num_input_tokens], + past_key_values=past_key_values_target, + use_cache=True, + logits_to_keep=1, + output_hidden_states=True, + ) + + output_ids[:, :num_input_tokens] = input_ids + output_ids[:, num_input_tokens : num_input_tokens + 1] = sample( + output.logits, temperature + ) + target_hidden = extract_context_feature( + output.hidden_states, self.target_layer_ids + ) + + # Decode stage + acceptance_lengths = [] + start = input_ids.shape[1] + while start < max_length: + block_output_ids = output_ids[:, start : start + block_size].clone() + block_position_ids = position_ids[:, start : start + block_size] + noise_embedding = target.model.embed_tokens(block_output_ids) + draft_logits = target.lm_head( + self( + target_hidden=target_hidden, + noise_embedding=noise_embedding, + position_ids=position_ids[ + :, past_key_values_draft.get_seq_length() : start + block_size + ], + past_key_values=past_key_values_draft, + use_cache=True, + is_causal=False, + )[:, -block_size + 1 :, :] + ) + past_key_values_draft.crop(start) + block_output_ids[:, 1:] = sample(draft_logits) + + output = target( + block_output_ids, + position_ids=block_position_ids, + past_key_values=past_key_values_target, + use_cache=True, + output_hidden_states=True, + ) + + posterior = sample(output.logits, temperature) + acceptance_length = ( + (block_output_ids[:, 1:] == posterior[:, :-1]) + .cumprod(dim=1) + .sum(dim=1)[0] + .item() + ) + output_ids[:, start : start + acceptance_length + 1] = block_output_ids[ + :, : acceptance_length + 1 + ] + output_ids[:, start + acceptance_length + 1] = posterior[ + :, acceptance_length + ] + start += acceptance_length + 1 + past_key_values_target.crop(start) + target_hidden = extract_context_feature( + output.hidden_states, self.target_layer_ids + )[:, : acceptance_length + 1, :] + acceptance_lengths.append(acceptance_length + 1) + if stop_token_ids is not None and any( + stop_token_id in output_ids[:, num_input_tokens:] + for stop_token_id in stop_token_ids + ): + break + output_ids = output_ids[:, :max_length] + output_ids = output_ids[:, output_ids[0] != self.mask_token_id] + if stop_token_ids is not None: + stop_token_ids = torch.tensor(stop_token_ids, device=output_ids.device) + stop_token_indices = torch.isin( + output_ids[0][num_input_tokens:], stop_token_ids + ).nonzero(as_tuple=True)[0] + if stop_token_indices.numel() > 0: + output_ids = output_ids[ + :, : num_input_tokens + stop_token_indices[0] + 1 + ] + + return output_ids diff --git a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py index 11d17151..e803ee27 100644 --- a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py +++ b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py @@ -193,7 +193,7 @@ def _prepare_model_kwargs(self, device: str) -> dict: Dictionary of model loading arguments """ default_kwargs = { - "dtype": torch.bfloat16, + "torch_dtype": torch.bfloat16, "device_map": device, "trust_remote_code": True, } @@ -228,6 +228,7 @@ def get_hidden_states_and_logits( attention_mask=attention_mask, output_hidden_states=True, output_logits=True, + use_cache=False, # match SpecForge: no KV-cache during training ) # Extract auxiliary hidden states @@ -914,7 +915,7 @@ def create_target_model( # Add backend-specific configuration if backend == "hf": - kwargs["dtype"] = torch_dtype + kwargs["torch_dtype"] = torch_dtype else: raise ValueError( f"Unsupported backend: '{backend}'. " diff --git a/angelslim/compressor/speculative/train/trainer/__init__.py b/angelslim/compressor/speculative/train/trainer/__init__.py index ad1c6729..62bc7970 100644 --- a/angelslim/compressor/speculative/train/trainer/__init__.py +++ b/angelslim/compressor/speculative/train/trainer/__init__.py @@ -13,6 +13,8 @@ # limitations under the License. from .offline_eagle3_trainer import OfflineEagle3Trainer, OfflineVLMEagle3Trainer +from .online_dflash_trainer import OnlineDFlashTrainer +from .offline_dflash_trainer import OfflineDFlashTrainer from .online_eagle3_trainer import ( OnlineEagle3Trainer, OnlineTTSEagle3Trainer, @@ -25,6 +27,8 @@ "OnlineEagle3Trainer", "OnlineVLMEagle3Trainer", "OnlineTTSEagle3Trainer", + "OnlineDFlashTrainer", + "OfflineDFlashTrainer", "OfflineEagle3Trainer", "OfflineVLMEagle3Trainer", ] diff --git a/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py b/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py new file mode 100644 index 00000000..e08c6a70 --- /dev/null +++ b/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py @@ -0,0 +1,44 @@ +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .online_dflash_trainer import OnlineDFlashTrainer +from .trainer_factory import Eagle3TrainerFactory + + +@Eagle3TrainerFactory.register("offline", "DFlash") +class OfflineDFlashTrainer(OnlineDFlashTrainer): + """ + DFlash trainer for offline (pre-computed hidden states) training. + + The main difference vs online: hidden_states are loaded directly from the + pre-computed .ckpt files, so prepare_data_for_draft_model() just unpacks + the batch instead of running a target-model forward pass. + """ + + def prepare_data_for_draft_model(self, inputs): + """ + Unpack pre-computed hidden states from the offline batch. + + Expected batch keys (from OfflineDFlashDataset): + input_ids [B, S] + hidden_states [B, S, D*L] + loss_mask [B, S] + attention_mask [B, S] + """ + return { + "input_ids": inputs["input_ids"], + "hidden_states": inputs["hidden_states"], + "loss_mask": inputs["loss_mask"], + "attention_mask": inputs["attention_mask"], + } diff --git a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py new file mode 100755 index 00000000..4040daa4 --- /dev/null +++ b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py @@ -0,0 +1,569 @@ +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Online DFlash Trainer for speculative decoding training. + +DFlash uses block-parallel cross-attention rather than Eagle3's +iterative autoregressive approach, so it overrides compute_loss +with its own block-wise CE loss logic. +""" + +import gc +import glob +import json +import os +from typing import Any, Dict, List, Optional, Tuple + +import torch +import torch.nn.functional as F +from torch import nn +from safetensors import safe_open +from transformers import AutoConfig + +from .eagle3_trainer import Eagle3Trainer +from .trainer_factory import Eagle3TrainerFactory + +try: + from torch.nn.attention.flex_attention import BlockMask, create_block_mask + FLEX_ATTENTION_AVAILABLE = True +except ImportError: + FLEX_ATTENTION_AVAILABLE = False + BlockMask = None + create_block_mask = None + + +def create_dflash_block_mask( + anchor_positions: torch.Tensor, + block_keep_mask: torch.Tensor, + S: int, + block_size: int, + device: torch.device, +): + """Construct Flex Attention BlockMask for DFlash training. + + KV: [Context (S tokens) | Block_0 | Block_1 | ... | Block_{n-1}] + Q: [Block_0 | Block_1 | ... | Block_{n-1}] + + Rules: + 1. Each block sees context strictly before its anchor (kv_idx < anchor_pos). + 2. Intra-block attention is bidirectional. + 3. Different blocks are invisible to each other. + 4. Invalid blocks (block_keep_mask=False) see nothing. + """ + + def dflash_mask_mod(b, h, q_idx, kv_idx): + q_block_id = q_idx // block_size + anchor_pos = anchor_positions[b, q_block_id] + + is_context = kv_idx < S + # Strictly less than: matches inference where target_hidden[anchor_pos] + # is not available as context. + mask_context = is_context & (kv_idx < anchor_pos) + + is_draft = kv_idx >= S + kv_block_id = (kv_idx - S) // block_size + mask_draft = is_draft & (q_block_id == kv_block_id) + + is_valid_block = block_keep_mask[b, q_block_id] + return (mask_context | mask_draft) & is_valid_block + + B, N = anchor_positions.shape + Q_LEN = N * block_size + KV_LEN = S + N * block_size + + return create_block_mask( + dflash_mask_mod, B=B, H=None, Q_LEN=Q_LEN, KV_LEN=KV_LEN, device=device + ) + + +class TargetEmbeddingsAndHead(nn.Module): + """Efficiently loads only the embedding layer and lm_head from a pretrained model. + + Handles safetensors slicing and Weight Tying correctly. + """ + + def __init__(self, config): + super().__init__() + self.config = config + + self.embed_tokens = nn.Embedding( + config.vocab_size, config.hidden_size, + padding_idx=getattr(config, "pad_token_id", None), + ) + + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + @classmethod + def from_pretrained( + cls, + model_path: str, + embed_key: Optional[str] = None, + lm_head_key: Optional[str] = None, + device: str = "cuda", + dtype: torch.dtype = torch.bfloat16, + trust_remote_code: bool = False, + ) -> "TargetEmbeddingsAndHead": + + config = AutoConfig.from_pretrained( + model_path, trust_remote_code=trust_remote_code + ) + instance = cls(config) + + if embed_key is None: + embed_key = "model.embed_tokens.weight" + if lm_head_key is None: + lm_head_key = "lm_head.weight" + + tie_weights = getattr(config, "tie_word_embeddings", False) + instance._load_weights(model_path, embed_key, lm_head_key, tie_weights) + + instance.to(device=device, dtype=dtype) + instance.eval() + instance.requires_grad_(False) + + return instance + + def _load_weights( + self, model_path: str, embed_key: str, lm_head_key: str, tie_weights: bool + ): + index_files = glob.glob(os.path.join(model_path, "*.index.json")) + files_to_load = {} + + if index_files: + with open(index_files[0], "r") as f: + index = json.load(f) + weight_map = index.get("weight_map", {}) + + if embed_key in weight_map: + files_to_load[embed_key] = weight_map[embed_key] + else: + raise ValueError( + f"Embedding key '{embed_key}' not found in weight map." + ) + + if not tie_weights: + if lm_head_key in weight_map: + files_to_load[lm_head_key] = weight_map[lm_head_key] + else: + safetensors = glob.glob(os.path.join(model_path, "*.safetensors")) + bins = glob.glob(os.path.join(model_path, "*.bin")) + target_file = safetensors[0] if safetensors else (bins[0] if bins else None) + + if not target_file: + raise FileNotFoundError("No checkpoint found.") + + files_to_load[embed_key] = os.path.basename(target_file) + if not tie_weights: + files_to_load[lm_head_key] = os.path.basename(target_file) + + file_to_keys_map = {} + for key, filename in files_to_load.items(): + full_path = os.path.join(model_path, filename) + if full_path not in file_to_keys_map: + file_to_keys_map[full_path] = [] + file_to_keys_map[full_path].append(key) + + for file_path, keys in file_to_keys_map.items(): + self._load_file_content(file_path, keys, embed_key, lm_head_key) + + if tie_weights: + self.lm_head.weight = self.embed_tokens.weight + + def _load_file_content( + self, + file_path: str, + keys_to_extract: list, + target_embed_key: str, + target_head_key: str, + ): + state_dict_part = {} + + if file_path.endswith(".safetensors"): + with safe_open(file_path, framework="pt") as f: + for k in keys_to_extract: + if k in f.keys(): + state_dict_part[k] = f.get_tensor(k) + else: + full_state = torch.load(file_path, map_location="cpu") + for k in keys_to_extract: + if k in full_state: + state_dict_part[k] = full_state[k] + del full_state + gc.collect() + + for k, tensor in state_dict_part.items(): + if k == target_embed_key: + self.embed_tokens.weight.data.copy_(tensor) + elif k == target_head_key: + if tensor.shape == self.lm_head.weight.data.shape: + self.lm_head.weight.data.copy_(tensor) + + +@Eagle3TrainerFactory.register("online", "DFlash") +class OnlineDFlashTrainer(Eagle3Trainer): + """Online DFlash Trainer for speculative decoding training. + + Uses block-parallel cross-attention and anchor-based CE loss + rather than Eagle3's iterative autoregressive training loop. + """ + + def __init__( + self, + draft_model: nn.Module, + target_model: nn.Module, + length: int, + draft_model_config: Dict[str, Any], + **kwargs, + ): + """ + Initialize the OnlineDFlashTrainer. + + Args: + draft_model: DFlash draft model + target_model: Target model for generating hidden states + length: Not used for DFlash (kept for interface compatibility) + draft_model_config: Configuration dictionary for draft model, + must contain dflash-specific fields + **kwargs: Additional arguments passed to parent Trainer + """ + super().__init__(draft_model=draft_model, length=length, **kwargs) + self.target_model = target_model + self._aux_hidden_states_layer_ids = getattr( + draft_model_config, "aux_hidden_states_layer_ids", None + ) + + # Extract DFlash-specific config + dflash_config = getattr(draft_model_config, "dflash_config", {}) or {} + self.block_size = getattr(draft_model_config, "block_size", 16) + self.num_anchors = getattr(draft_model_config, "num_anchors", 512) + self.loss_decay_gamma = getattr(draft_model_config, "loss_decay_gamma", None) + self.attention_backend = getattr( + draft_model_config, "attention_backend", "flex_attention" + ) + self.mask_token_id = dflash_config.get( + "mask_token_id", + getattr(draft_model_config, "mask_token_id", None), + ) + + # Load target model's lm_head and embed_tokens + # In offline mode target_model may be None; fall back to config path. + target_model_path = None + if target_model is not None: + target_model_path = getattr(target_model, "model_path", None) + if target_model_path is None: + target_model_path = getattr( + draft_model_config, "target_model_name_or_path", None + ) + embed_weight_key = getattr( + draft_model_config, "embed_weight_key", "model.embed_tokens.weight" + ) + lm_head_key = getattr(draft_model_config, "lm_head_key", "lm_head.weight") + trust_remote_code = getattr(draft_model_config, "trust_remote_code", True) + + if target_model_path is not None: + target_components = TargetEmbeddingsAndHead.from_pretrained( + target_model_path, + embed_key=embed_weight_key, + lm_head_key=lm_head_key, + device="cuda", + trust_remote_code=trust_remote_code, + ) + self.target_lm_head = target_components.lm_head + self.target_embed_tokens = target_components.embed_tokens + else: + raise ValueError( + "target_model_name_or_path must be set in draft_model_config " + "or target_model.model_path for DFlash training." + ) + + def prepare_data_for_draft_model(self, inputs): + """Prepare data for DFlash training. + + Extracts hidden states from the target model. DFlash needs + multi-layer hidden states concatenated as context features. + """ + input_ids = inputs["input_ids"] + attention_mask = inputs["attention_mask"] + loss_mask = inputs["loss_mask"] + + # Get hidden states from target model + hidden_states, _ = self.target_model.get_hidden_states_and_logits( + input_ids=input_ids, + attention_mask=attention_mask, + aux_hidden_states_layer_ids=self._aux_hidden_states_layer_ids, + ) + + return { + "input_ids": input_ids, + "hidden_states": hidden_states, + "loss_mask": loss_mask, + "attention_mask": attention_mask, + } + + def _sample_anchor_positions( + self, seq_len: int, loss_mask: torch.Tensor, device: torch.device + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Randomly sample anchor positions per sample; returns (anchors, keep_mask). + + Returns (None, None) when the batch has no valid anchors (too-short or + loss_mask-empty sequences), which is handled gracefully in forward(). + """ + bs = self.block_size + bsz = loss_mask.shape[0] + max_anchor = max(seq_len - bs, 0) + + valid = loss_mask[:, : max_anchor + 1] > 0.5 + valid_counts = valid.sum(dim=1) + max_valid = int(valid_counts.max().item()) + + # Need at least 2 valid positions (anchor + at least one prediction target) + if max_valid <= 1: + return None, None + + max_n = min(self.num_anchors, max_valid - 1) + + indices = ( + torch.arange(max_anchor + 1, device=device).unsqueeze(0).expand(bsz, -1) + ) + masked_indices = torch.where( + valid, indices, torch.tensor(seq_len + 1, device=device) + ) + + random_vals = torch.rand(bsz, max_anchor + 1, device=device) + random_vals = torch.where(valid, random_vals, torch.tensor(2.0, device=device)) + + _, sorted_idx = random_vals.sort(dim=1) + gathered = torch.gather(masked_indices, 1, sorted_idx) + anchors = gathered[:, :max_n].sort(dim=1).values + + keep_mask = torch.arange(max_n, device=device).unsqueeze( + 0 + ) < valid_counts.unsqueeze(1).clamp(max=max_n) + anchors = torch.where( + keep_mask, anchors, torch.tensor(0, dtype=torch.long, device=device) + ) + + return anchors, keep_mask + + def _create_position_ids(self, anchor_positions: torch.Tensor) -> torch.Tensor: + """Create absolute position IDs for parallel draft blocks.""" + bsz, n_blocks = anchor_positions.shape + device = anchor_positions.device + offsets = torch.arange(self.block_size, device=device).view(1, 1, -1) + pos_ids = anchor_positions.unsqueeze(-1) + offsets + return pos_ids.view(bsz, -1) + + def _create_noise_embed(self, input_ids, anchor_positions, block_keep_mask): + bsz, seq_len = input_ids.shape + n = anchor_positions.shape[1] + bs = self.block_size + device = input_ids.device + + noise_ids = torch.full( + (bsz, n * bs), self.mask_token_id, dtype=torch.long, device=device + ) + + block_starts = torch.arange(n, device=device) * bs + block_starts = block_starts.unsqueeze(0).expand(bsz, -1) + + valid_anchor_positions = anchor_positions.clamp(0, seq_len - 1) + anchor_tokens = torch.gather(input_ids, 1, valid_anchor_positions) + + flat_batch_idx = torch.arange(bsz, device=device).unsqueeze(1).expand(bsz, n) + noise_ids[flat_batch_idx, block_starts] = torch.where( + block_keep_mask, + anchor_tokens, + torch.tensor(self.mask_token_id, dtype=torch.long, device=device), + ) + + return self.target_embed_tokens(noise_ids) + + def _compute_dflash_loss_and_accuracy( + self, + model: nn.Module, + input_ids: torch.Tensor, + hidden_states: torch.Tensor, + loss_mask: torch.Tensor, + ): + """Core DFlash block-parallel loss logic (shared by train + eval). + + Steps: + 1. Sample anchor positions from valid loss_mask positions. + 2. Build noise embedding (anchor token is real, rest are MASK). + 3. Build DFlash BlockMask (context-causal + intra-block bidirectional). + 4. Run draft model forward → logits. + 5. Compute weighted CE loss with optional exponential decay. + 6. Compute accuracy (no-decay mask). + + Returns: + (loss, accuracy) — both scalar tensors. + """ + bsz, seq_len = input_ids.shape + device = input_ids.device + + # ── 1. Anchor sampling ──────────────────────────────────────────────── + anchor_positions, block_keep_mask = self._sample_anchor_positions( + seq_len, loss_mask, device + ) + + # No valid anchors → return zero loss connected to model params (DDP-safe) + if anchor_positions is None: + zero_loss = sum( + p.sum() * 0.0 + for p in model.parameters() + if p.requires_grad + ) + return zero_loss, torch.tensor(0.0, device=device) + + # ── 2. Noise embedding ──────────────────────────────────────────────── + noise_embedding = self._create_noise_embed( + input_ids, anchor_positions, block_keep_mask + ) + + # ── 3. Position IDs [B, S + N*block_size] ─────────────────────────── + context_position_ids = ( + torch.arange(seq_len, device=device).unsqueeze(0).expand(bsz, -1) + ) + draft_position_ids = self._create_position_ids(anchor_positions) + full_position_ids = torch.cat([context_position_ids, draft_position_ids], dim=1) + + # ── 4. Attention mask (DFlash BlockMask) ───────────────────────────── + dflash_attn_mask = create_dflash_block_mask( + anchor_positions=anchor_positions, + block_keep_mask=block_keep_mask, + S=seq_len, + block_size=self.block_size, + device=device, + ) + + # ── 5. Draft model forward → logits [B, N*bs, vocab] ──────────────── + model_dtype = next(model.parameters()).dtype + noise_embedding = noise_embedding.to(model_dtype) + hidden_states = hidden_states.to(model_dtype) + + output_hidden = model( + noise_embedding=noise_embedding, + target_hidden=hidden_states, + attention_mask=dflash_attn_mask, + position_ids=full_position_ids, + ) + + output_hidden = output_hidden.to(self.target_lm_head.weight.dtype) + logits = self.target_lm_head(output_hidden) + + # ── 6. Labels: position k in block predicts token at (anchor + k) ──── + bs = self.block_size + label_offsets = torch.arange(0, bs, device=device).view(1, 1, -1) + label_indices = anchor_positions.unsqueeze(-1) + label_offsets + valid_label_mask = label_indices < seq_len + safe_label_indices = label_indices.clamp(max=seq_len - 1) + + target_ids = torch.gather( + input_ids.unsqueeze(1).expand(-1, anchor_positions.size(1), -1), + dim=2, + index=safe_label_indices, + ) # [B, N, bs] + + # ── 7. Weight mask: valid block × in-bounds × skip anchor × loss_mask ─ + weight_mask = ( + block_keep_mask.unsqueeze(-1).expand(-1, -1, bs).float() + ) + weight_mask = weight_mask * valid_label_mask.float() + + pos_in_block = torch.arange(bs, device=device).view(1, 1, -1) + weight_mask = weight_mask * (pos_in_block > 0).float() # skip pos 0 (anchor) + + gathered_loss_mask = torch.gather( + loss_mask.unsqueeze(1).expand(-1, anchor_positions.size(1), -1), + dim=2, + index=safe_label_indices, + ) + weight_mask = weight_mask * gathered_loss_mask + + binary_eval_mask = weight_mask.view(-1) # no decay, used for accuracy + + # ── 8. Exponential decay: exp(-(k-1)/γ), k=1 gets weight 1.0 ───────── + if self.loss_decay_gamma is not None and self.loss_decay_gamma > 0: + k = torch.arange(bs, device=device).view(1, 1, -1) + decay = torch.exp(-(k - 1).clamp(min=0).float() / self.loss_decay_gamma) + weight_mask = weight_mask * decay + + # ── 9. Cross-entropy loss ───────────────────────────────────────────── + flat_logits = logits.view(-1, logits.size(-1)) + flat_targets = target_ids.view(-1) + flat_weights = weight_mask.view(-1) + + loss_per_token = F.cross_entropy(flat_logits, flat_targets, reduction="none") + loss = (loss_per_token * flat_weights).sum() / (flat_weights.sum() + 1e-6) + + # ── 10. Accuracy (no gradient) ──────────────────────────────────────── + with torch.no_grad(): + pred_ids = torch.argmax(flat_logits, dim=-1) + correct = (pred_ids == flat_targets) & (binary_eval_mask > 0.5) + accuracy = correct.sum().float() / (binary_eval_mask.sum() + 1e-6) + + return loss, accuracy + + def compute_loss( + self, + model: nn.Module, + inputs: Dict[str, torch.Tensor], + num_items_in_batch: Optional[int] = None, + return_outputs: bool = False, + ) -> torch.Tensor: + """Compute the DFlash training loss. + + Unlike Eagle3's iterative multi-step loss, DFlash computes a single + block-parallel cross-entropy loss over all sampled anchor positions. + """ + data = self.prepare_data_for_draft_model(inputs) + + loss, accuracy = self._compute_dflash_loss_and_accuracy( + model=model, + input_ids=data["input_ids"], + hidden_states=data["hidden_states"], + loss_mask=data["loss_mask"], + ) + + self.log({ + "train/loss": round(float(loss.item()), 4), + "train/accuracy": round(float(accuracy.item()), 4), + }) + + return loss + + def prediction_step( + self, + model: nn.Module, + inputs: Dict[str, torch.Tensor], + prediction_loss_only: bool, + ignore_keys: Optional[List[str]] = None, + ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor]]: + """Perform an evaluation step.""" + data = self.prepare_data_for_draft_model(inputs) + + with torch.no_grad(): + loss, accuracy = self._compute_dflash_loss_and_accuracy( + model=model, + input_ids=data["input_ids"], + hidden_states=data["hidden_states"], + loss_mask=data["loss_mask"], + ) + + self.log({ + "eval/loss": round(float(loss.item()), 4), + "eval/accuracy": round(float(accuracy.item()), 4), + }) + + return loss, None, None diff --git a/angelslim/engine.py b/angelslim/engine.py index 12b97ea6..b4002a8f 100644 --- a/angelslim/engine.py +++ b/angelslim/engine.py @@ -532,7 +532,7 @@ def run( print_info("=" * 80) for i, output in enumerate(outputs[:5]): generated_text = output.outputs[0].text - print_info(f"[{i+1}] Output: {generated_text!r}") + print_info(f"[{i + 1}] Output: {generated_text!r}") print_info(f"\nTotal outputs generated: {len(outputs)}") # Collect and save statistics diff --git a/angelslim/models/llm/tiktoken_tokenizer.py b/angelslim/models/llm/tiktoken_tokenizer.py index 243a5b23..63691c22 100644 --- a/angelslim/models/llm/tiktoken_tokenizer.py +++ b/angelslim/models/llm/tiktoken_tokenizer.py @@ -8,7 +8,26 @@ import tiktoken from tiktoken.load import load_tiktoken_bpe from tokenizers import AddedToken -from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode +try: + from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode +except ImportError: + # bytes_to_unicode was removed from newer transformers versions. + # Inline the original GPT-2 implementation. + def bytes_to_unicode(): + bs = ( + list(range(ord("!"), ord("~") + 1)) + + list(range(ord("¡"), ord("¬") + 1)) + + list(range(ord("®"), ord("ÿ") + 1)) + ) + cs = bs[:] + n = 0 + for b in range(2**8): + if b not in bs: + bs.append(b) + cs.append(2**8 + n) + n += 1 + cs = [chr(c) for c in cs] + return dict(zip(bs, cs)) from transformers.tokenization_utils import PreTrainedTokenizer logger = getLogger(__name__) diff --git a/angelslim/models/omni/qwen3_omni.py b/angelslim/models/omni/qwen3_omni.py index 4dbdf933..20886134 100644 --- a/angelslim/models/omni/qwen3_omni.py +++ b/angelslim/models/omni/qwen3_omni.py @@ -17,9 +17,13 @@ from transformers import ( AutoProcessor, AutoTokenizer, - Qwen3OmniMoeForConditionalGeneration, ) +try: + from transformers import Qwen3OmniMoeForConditionalGeneration +except ImportError: + Qwen3OmniMoeForConditionalGeneration = None # not available in this transformers version + from ...compressor.quant.core import PTQVLMSaveVllmHF from ...utils import find_layers, print_info from ..base_model import BaseLLMModel diff --git a/angelslim/models/vlm/qwen3_vl.py b/angelslim/models/vlm/qwen3_vl.py index 183db3ed..52efa925 100644 --- a/angelslim/models/vlm/qwen3_vl.py +++ b/angelslim/models/vlm/qwen3_vl.py @@ -15,7 +15,12 @@ import torch import torch.nn.functional as F from tqdm import tqdm -from transformers import AutoProcessor, AutoTokenizer, Qwen3VLForConditionalGeneration +from transformers import AutoProcessor, AutoTokenizer + +try: + from transformers import Qwen3VLForConditionalGeneration +except ImportError: + Qwen3VLForConditionalGeneration = None # not available in this transformers version from ...compressor.quant.core import LossFilter, PTQVLMSaveVllmHF from ...utils import find_layers, print_info diff --git a/angelslim/models/vlm/qwen3_vl_moe.py b/angelslim/models/vlm/qwen3_vl_moe.py index f3f311c2..37fc085d 100644 --- a/angelslim/models/vlm/qwen3_vl_moe.py +++ b/angelslim/models/vlm/qwen3_vl_moe.py @@ -21,9 +21,14 @@ from transformers import ( AutoProcessor, AutoTokenizer, - Qwen3VLMoeForConditionalGeneration, ) -from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts + +try: + from transformers import Qwen3VLMoeForConditionalGeneration + from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts +except ImportError: + Qwen3VLMoeForConditionalGeneration = None # not available in this transformers version + Qwen3VLMoeTextExperts = None from angelslim.compressor.quant.core.quant_func import get_fp_maxval from angelslim.compressor.quant.observers import MoEAbsmaxPertensorObserver diff --git a/configs/qwen3_dflash.json b/configs/qwen3_dflash.json new file mode 100755 index 00000000..b58bb0c7 --- /dev/null +++ b/configs/qwen3_dflash.json @@ -0,0 +1,54 @@ +{ + "architectures": [ + "QwenDFlashDraftModel" + ], + "attention_bias": false, + "attention_dropout": 0.0, + "block_size": 16, + "bos_token_id": 151643, + "dflash_config": { + "mask_token_id": 151669, + "target_layer_ids": [ + 1, + 9, + 17, + 25, + 33 + ] + }, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2560, + "initializer_range": 0.02, + "intermediate_size": 9728, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "max_position_embeddings": 40960, + "max_window_layers": 5, + "model_type": "qwen3", + "num_attention_heads": 32, + "num_hidden_layers": 5, + "num_key_value_heads": 8, + "num_target_layers": 36, + "rms_norm_eps": 1e-06, + "rope_scaling": null, + "rope_theta": 1000000, + "sliding_window": null, + "tie_word_embeddings": true, + "torch_dtype": "bfloat16", + "transformers_version": "4.57.3", + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 151936, + + "num_anchors": 512, + "loss_decay_gamma": 7.0, + "attention_backend": "flex_attention" +} diff --git a/scripts/speculative/generate_dflash_data.sh b/scripts/speculative/generate_dflash_data.sh new file mode 100644 index 00000000..ce7a8706 --- /dev/null +++ b/scripts/speculative/generate_dflash_data.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# ============================================================================= +# Step 1: Pre-generate DFlash training data (hidden states) from target model. +# +# Usage: +# bash scripts/speculative/generate_qwen3_dflash_data.sh [NUM_GPUS] +# +# Output: +# One .ckpt file per training sample, saved to OUTPUT_DIR. +# Each file contains: input_ids, hidden_states (5-layer concat), loss_mask, +# attention_mask. +# ============================================================================= + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) + +# Use local source code instead of installed site-packages +export PYTHONPATH=$ROOT_DIR:$PYTHONPATH + +NUM_GPUS=${1:-8} + +# ---- Paths -- modify these to match your environment ---- +TARGET_MODEL_PATH="" +TRAIN_DATA_PATH="" +OUTPUT_DIR="${ROOT_DIR}/outputs/" # directory for .ckpt files + +torchrun \ + --standalone \ + --nproc_per_node $NUM_GPUS \ + $ROOT_DIR/tools/generate_dflash_data.py \ + --target_model_name_or_path $TARGET_MODEL_PATH \ + --draft_model_config_path $ROOT_DIR/configs/qwen3_dflash.json \ + --train_data_path $TRAIN_DATA_PATH \ + --output_dir $OUTPUT_DIR \ + --model_max_length 3072 \ + --chat_template_type qwen3 \ + --batch_size 1 \ + --num_proc 16 \ + --sample_num 128 \ + --shard_size 10000 diff --git a/scripts/speculative/run_dflash_offline.sh b/scripts/speculative/run_dflash_offline.sh new file mode 100644 index 00000000..05524719 --- /dev/null +++ b/scripts/speculative/run_dflash_offline.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# ============================================================================= +# Step 2: Train DFlash draft model in OFFLINE mode. +# +# Prerequisites: +# Run generate_qwen3_dflash_data.sh first to produce the .ckpt files. +# +# Usage: +# bash scripts/speculative/run_qwen3_dflash_offline.sh [NUM_GPUS] +# ============================================================================= + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) + +# Use local source code instead of installed site-packages +export PYTHONPATH=$ROOT_DIR:$PYTHONPATH + +NUM_GPUS=${1:-8} + +# ---- Paths -- modify these to match your environment ---- +TARGET_MODEL_PATH="" +TRAIN_HIDDEN_PATH="" +OUTPUT_DIR="${ROOT_DIR}/outputs/" + +# WandB configuration +export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflash-offline"} + +torchrun \ + --standalone \ + --nproc_per_node $NUM_GPUS \ + $ROOT_DIR/tools/train_dflash_offline.py \ + --target_model_name_or_path $TARGET_MODEL_PATH \ + --draft_model_config_path $ROOT_DIR/configs/qwen3_dflash.json \ + --train_hidden_path $TRAIN_HIDDEN_PATH \ + --output_dir $OUTPUT_DIR \ + --num_train_epochs 12 \ + --per_device_train_batch_size 2 \ + --learning_rate 6e-4 \ + --warmup_ratio 0.04 \ + --max_grad_norm 1.0 \ + --model_max_length 3072 \ + --chat_template_type qwen3 \ + --attention_backend flex_attention \ + --block_size 16 \ + --num_anchors 512 \ + --loss_decay_gamma 7.0 \ + --logging_steps 50 \ + --save_strategy steps \ + --save_steps 2500 \ + --bf16 \ + --lr_scheduler_type cosine \ + --report_to wandb \ + --run_name $WANDB_RUN_NAME diff --git a/scripts/speculative/run_dflash_online.sh b/scripts/speculative/run_dflash_online.sh new file mode 100644 index 00000000..1d4a1397 --- /dev/null +++ b/scripts/speculative/run_dflash_online.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# DFlash Online Training Script for Qwen3 +# Usage: bash scripts/speculative/run_qwen3_dflash_online.sh [NUM_GPUS] [ATTENTION_BACKEND] + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) + +# Use local source code instead of installed site-packages +export PYTHONPATH=$ROOT_DIR:$PYTHONPATH + +NUM_GPUS=${1:-8} +ATTENTION_BACKEND=${2:-flex_attention} + +# Set paths - modify these to match your environment +TARGET_MODEL_PATH="" +TRAIN_DATA_PATH="" +OUTPUT_DIR="${ROOT_DIR}/outputs/" + +export CONFIG_DIR=${ROOT_DIR}/angelslim/compressor/speculative/train/configs + +# WandB configuration (mirrors SpecForge's --wandb-project / --wandb-name) +export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflash"} + +torchrun \ + --standalone \ + --nproc_per_node $NUM_GPUS \ + $ROOT_DIR/tools/train_dflash_online.py \ + --target_model_name_or_path $TARGET_MODEL_PATH \ + --draft_model_config_path $ROOT_DIR/configs/qwen3_dflash.json \ + --train_data_path $TRAIN_DATA_PATH \ + --output_dir $OUTPUT_DIR \ + --modal_type DFlash \ + --training_mode online \ + --num_train_epochs 6 \ + --per_device_train_batch_size 2 \ + --learning_rate 6e-4 \ + --warmup_ratio 0.04 \ + --max_grad_norm 1.0 \ + --model_max_length 3072 \ + --chat_template_type qwen3 \ + --attention_backend $ATTENTION_BACKEND \ + --block_size 16 \ + --num_anchors 512 \ + --loss_decay_gamma 7.0 \ + --logging_steps 50 \ + --save_strategy steps \ + --save_steps 2500 \ + --bf16 \ + --lr_scheduler_type cosine \ + --report_to wandb \ + --run_name $WANDB_RUN_NAME + diff --git a/tools/generate_dflash_data.py b/tools/generate_dflash_data.py new file mode 100755 index 00000000..b4d1e345 --- /dev/null +++ b/tools/generate_dflash_data.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# DFlash offline data pre-generation script. +# +# Usage: +# torchrun --nproc_per_node=8 tools/generate_dflash_data.py \ +# --target_model_name_or_path /path/to/Qwen3-4B \ +# --draft_model_config_path configs/qwen3_dflash.json \ +# --train_data_path /path/to/data.jsonl \ +# --output_dir /path/to/output/ckpts \ +# --model_max_length 3072 \ +# --chat_template_type qwen3 +# +# Each output .ckpt file contains: +# - input_ids: LongTensor [1, S] +# - hidden_states: BFloat16Tensor [1, S, D*num_target_layers] (multi-layer concat) +# - loss_mask: LongTensor [1, S] +# - attention_mask: LongTensor [1, S] + +import argparse +import os +import time +from pathlib import Path + +import torch +import torch.distributed as dist +from torch.utils.data import DataLoader, DistributedSampler + +from angelslim.compressor.speculative import ( + DatasetManager, + DraftModelConfig, + create_target_model, + get_supported_chat_template_type_strings, +) +from angelslim.utils import rank0_print + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Pre-generate DFlash training data (hidden states) from target model" + ) + + # Model + parser.add_argument("--target_model_name_or_path", type=str, required=True) + parser.add_argument("--draft_model_config_path", type=str, required=True) + parser.add_argument( + "--target_backend", type=str, default="hf", choices=["hf"], + help="Target model backend", + ) + parser.add_argument( + "--torch_dtype", type=str, default="bfloat16", + choices=["float16", "bfloat16", "float32"], + ) + parser.add_argument("--trust_remote_code", action="store_true", default=True) + + # Data + parser.add_argument("--train_data_path", type=str, nargs="+", required=True, + help="Input JSONL file(s)") + parser.add_argument("--output_dir", type=str, required=True, + help="Directory to save .ckpt files") + parser.add_argument( + "--chat_template_type", type=str, default="qwen3", + help=f"Supported: {', '.join(get_supported_chat_template_type_strings())}", + ) + parser.add_argument("--model_max_length", type=int, default=3072) + parser.add_argument("--block_size", type=int, default=16, + help="Block size for DFlash parallel prediction") + parser.add_argument("--num_proc", type=int, default=16, + help="Workers for tokenization (dataset.map)") + parser.add_argument("--batch_size", type=int, default=1, + help="Samples per forward pass (keep at 1 for variable-length seqs)") + parser.add_argument("--shuffle_seed", type=int, default=42) + parser.add_argument("--sample_num", type=int, default=None, + help="Limit number of samples (for debugging)") + parser.add_argument("--shard_size", type=int, default=0, + help="Save a new sub-directory every N files (0 = no sharding)") + + return parser.parse_args() + + +def get_local_rank(): + return int(os.environ.get("LOCAL_RANK", 0)) + + +def get_global_rank(): + return int(os.environ.get("RANK", 0)) + + +def get_world_size(): + return int(os.environ.get("WORLD_SIZE", 1)) + + +def init_distributed(): + if get_world_size() > 1 and not dist.is_initialized(): + dist.init_process_group(backend="nccl") + local_rank = get_local_rank() + torch.cuda.set_device(local_rank) + + +def main(): + args = parse_args() + init_distributed() + + rank = get_global_rank() + world_size = get_world_size() + local_rank = get_local_rank() + + # -------------------------------------------------------------------------- + # 1. Load draft-model config (to get target_layer_ids) + # -------------------------------------------------------------------------- + rank0_print("Loading draft model config...") + draft_model_config = DraftModelConfig.from_file(args.draft_model_config_path) + dflash_config = getattr(draft_model_config, "dflash_config", {}) or {} + target_layer_ids = dflash_config.get("target_layer_ids", None) + if target_layer_ids is None: + raise ValueError( + "dflash_config.target_layer_ids not found in draft_model_config. " + f"Please set it in {args.draft_model_config_path}" + ) + rank0_print(f"DFlash target layer IDs: {target_layer_ids}") + + # -------------------------------------------------------------------------- + # 2. Load target model (on this rank's GPU) + # -------------------------------------------------------------------------- + dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} + torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) + + rank0_print("Loading target model...") + target_model = create_target_model( + backend=args.target_backend, + model_path=args.target_model_name_or_path, + modal_type="LLM", + torch_dtype=torch_dtype, + trust_remote_code=args.trust_remote_code, + ) + rank0_print("Target model loaded successfully") + + # -------------------------------------------------------------------------- + # 3. Tokenize dataset (using DatasetManager, same as online training) + # -------------------------------------------------------------------------- + rank0_print("Building dataset...") + # Temporarily patch args so DatasetManager picks the correct builder + args.modal_type = "LLM" # DFlash uses the LLM tokenisation path + args.training_mode = "online" # We want the text→token builder, not offline .ckpt loader + + from transformers import AutoTokenizer + tokenizer = AutoTokenizer.from_pretrained( + args.target_model_name_or_path, trust_remote_code=True + ) + + dataset_manager = DatasetManager( + data_args=args, + tokenizer=tokenizer, + model_max_length=args.model_max_length, + chat_template_type=args.chat_template_type, + ) + + # Restore modal_type to DFlash so DFlash-specific filtering (min_loss_tokens) applies + args.modal_type = "DFlash" + + ( + _, # offline_train_dataset (unused here) + _, # offline_eval_dataset + online_train_dataset, + _, # online_eval_dataset + _, # data_collator + ) = dataset_manager.create_all_datasets() + + if online_train_dataset is None: + raise RuntimeError("No training dataset was created. Check --train_data_path.") + + rank0_print(f"Dataset size: {len(online_train_dataset)}") + + # -------------------------------------------------------------------------- + # 4. Distributed sampler: each rank processes its own shard + # -------------------------------------------------------------------------- + sampler = DistributedSampler( + online_train_dataset, + num_replicas=world_size, + rank=rank, + shuffle=False, + drop_last=False, + ) + + def collate_fn(batch): + """Simple collate: each element is already a dict of 1-D tensors.""" + result = {} + for key in batch[0]: + tensors = [item[key] for item in batch] + # Tensors may be 1-D or 2-D ([1, S]) — keep original shape + try: + result[key] = torch.stack(tensors) + except Exception: + result[key] = tensors + return result + + dataloader = DataLoader( + online_train_dataset, + batch_size=args.batch_size, + sampler=sampler, + num_workers=4, + pin_memory=True, + collate_fn=collate_fn, + ) + + # -------------------------------------------------------------------------- + # 5. Output directory setup + # -------------------------------------------------------------------------- + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + rank0_print(f"Saving .ckpt files to: {output_dir}") + rank0_print(f"World size={world_size}, this rank={rank}") + + # -------------------------------------------------------------------------- + # 6. Main loop: forward target model, save hidden states + # -------------------------------------------------------------------------- + global_idx = 0 # index within this rank's portion + total = len(dataloader) + t0 = time.time() + + for batch_idx, batch in enumerate(dataloader): + input_ids = batch["input_ids"] + # Shape may be [B, 1, S] or [B, S] depending on how dataset stores it + if input_ids.dim() == 3: + input_ids = input_ids.squeeze(1) # → [B, S] + + attention_mask = batch.get("attention_mask", torch.ones_like(input_ids)) + if attention_mask.dim() == 3: + attention_mask = attention_mask.squeeze(1) + + loss_mask = batch["loss_mask"] + if loss_mask.dim() == 3: + loss_mask = loss_mask.squeeze(1) + + input_ids = input_ids.to(f"cuda:{local_rank}") + attention_mask = attention_mask.to(f"cuda:{local_rank}") + + # Run target model + hidden_states, _ = target_model.get_hidden_states_and_logits( + input_ids=input_ids, + attention_mask=attention_mask, + aux_hidden_states_layer_ids=target_layer_ids, + ) + # hidden_states: [B, S, D*len(target_layer_ids)] + + # Save one .ckpt per sample in the batch + for i in range(input_ids.size(0)): + # Use a globally unique name: rank × position within rank + sample_idx = rank * len(dataloader) + global_idx + global_idx += 1 + + if args.shard_size > 0: + shard_id = sample_idx // args.shard_size + save_dir = output_dir / f"shard_{shard_id:05d}" + save_dir.mkdir(parents=True, exist_ok=True) + else: + save_dir = output_dir + + ckpt_path = save_dir / f"sample_{sample_idx:08d}_rank{rank}.ckpt" + + ckpt = { + "input_ids": input_ids[i : i + 1].cpu(), # [1, S] + "hidden_states": hidden_states[i : i + 1].cpu().to(torch.bfloat16), # [1, S, D*L] + "loss_mask": loss_mask[i : i + 1].cpu(), # [1, S] + "attention_mask": attention_mask[i : i + 1].cpu(), # [1, S] + } + torch.save(ckpt, ckpt_path) + + # Progress log + if batch_idx % 100 == 0: + elapsed = time.time() - t0 + samples_done = (batch_idx + 1) * args.batch_size + speed = samples_done / elapsed if elapsed > 0 else 0 + rank0_print( + f"[rank {rank}] {batch_idx + 1}/{total} batches | " + f"{speed:.1f} samples/s | elapsed {elapsed:.0f}s" + ) + + if world_size > 1: + dist.barrier() + + rank0_print( + f"Data generation complete. " + f"Saved files to {output_dir}" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/train_dflash_offline.py b/tools/train_dflash_offline.py new file mode 100755 index 00000000..a9231b89 --- /dev/null +++ b/tools/train_dflash_offline.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# DFlash offline training script. +# Trains a DFlash draft model using pre-computed hidden states (.ckpt files). +# +# Workflow: +# Step 1 (data generation): +# bash scripts/speculative/generate_qwen3_dflash_data.sh +# Step 2 (offline training, this script): +# bash scripts/speculative/run_qwen3_dflash_offline.sh + +import argparse +import os +from pathlib import Path + +import torch +import transformers +from transformers import AutoTokenizer + +from angelslim.compressor.speculative import ( + DraftModelConfig, + Eagle3TrainerFactory, + create_draft_model, + get_supported_chat_template_type_strings, +) + +from angelslim.compressor.speculative.train.models.draft.online_dflash_model import ( + OnlineDFlashModel, +) +from angelslim.compressor.speculative.train.trainer.online_dflash_trainer import ( + OnlineDFlashTrainer, +) +from angelslim.compressor.speculative.train.data.dataset_builder.offline_dataset_builder import ( + OfflineEagle3Dataset, +) +from angelslim.compressor.speculative.train.data.data_utils import DataCollatorWithPadding +from angelslim.utils import rank0_print + + +# --------------------------------------------------------------------------- +# Offline DFlash Dataset +# --------------------------------------------------------------------------- + +class OfflineDFlashDataset(OfflineEagle3Dataset): + """ + DFlash variant of the offline dataset. + + Each .ckpt file must contain: + - input_ids: LongTensor [1, S] + - hidden_states: BFloat16Tensor [1, S, D*num_target_layers] ← multi-layer hidden states + - loss_mask: LongTensor [1, S] + - attention_mask: LongTensor [1, S] (auto-generated if missing) + + Note: DFlash does NOT need target_hiddens (only Eagle3 offline uses the + single final-layer hidden state). The multi-layer hidden_states is the + context feature passed directly to the DFlash cross-attention. + """ + + REQUIRED_KEYS = ["input_ids", "hidden_states", "loss_mask"] + + def _load_ckpt(self, idx: int): + import warnings + ckpt_path = self.ckpt_files[idx] + try: + data = torch.load(ckpt_path, map_location="cpu", weights_only=False) + except Exception as e: + warnings.warn(f"Failed to load {ckpt_path}: {e}. Skipping.", RuntimeWarning) + return None + + missing = [k for k in self.REQUIRED_KEYS if k not in data] + if missing: + warnings.warn( + f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning + ) + return None + + # Auto-generate attention_mask if absent + if "attention_mask" not in data: + data["attention_mask"] = torch.ones_like(data["input_ids"]) + + return data + + +# --------------------------------------------------------------------------- +# Argument parser +# --------------------------------------------------------------------------- + +def parse_args(): + parser = argparse.ArgumentParser(description="Train DFlash draft model (offline mode)") + + # Model + m = parser.add_argument_group("Model Arguments") + m.add_argument("--target_model_name_or_path", type=str, required=True) + m.add_argument("--draft_model_config_path", type=str, required=True) + m.add_argument("--torch_dtype", type=str, default="bfloat16", + choices=["float16", "bfloat16", "float32"]) + m.add_argument("--trust_remote_code", action="store_true", default=True) + m.add_argument("--embed_weight_key", type=str, default="model.embed_tokens.weight") + m.add_argument("--lm_head_key", type=str, default="lm_head.weight") + + # DFlash-specific (override values in config JSON) + d = parser.add_argument_group("DFlash Arguments") + d.add_argument("--block_size", type=int, default=None) + d.add_argument("--num_anchors", type=int, default=None) + d.add_argument("--loss_decay_gamma", type=float, default=None) + d.add_argument("--mask_token_id", type=int, default=None) + d.add_argument("--attention_backend", type=str, default=None, + choices=["flex_attention", "sdpa", "eager"]) + + # Data + da = parser.add_argument_group("Data Arguments") + da.add_argument("--train_hidden_path", type=str, required=True, + help="Directory of pre-computed training .ckpt files") + da.add_argument("--eval_hidden_path", type=str, default=None, + help="Directory of pre-computed eval .ckpt files (optional)") + da.add_argument("--chat_template_type", type=str, default="qwen3", + help=f"Supported: {', '.join(get_supported_chat_template_type_strings())}") + da.add_argument("--model_max_length", type=int, default=3072) + da.add_argument("--num_proc", type=int, default=16) + da.add_argument("--cache_in_memory", action="store_true", default=False, + help="Cache all .ckpt files in RAM (fast but memory-intensive)") + + # Training + t = parser.add_argument_group("Training Arguments") + t.add_argument("--output_dir", type=str, required=True) + t.add_argument("--optim", type=str, default="adamw_torch") + t.add_argument("--num_train_epochs", type=int, default=6) + t.add_argument("--per_device_train_batch_size", type=int, default=2) + t.add_argument("--per_device_eval_batch_size", type=int, default=2) + t.add_argument("--gradient_accumulation_steps", type=int, default=1) + t.add_argument("--learning_rate", type=float, default=6e-4) + t.add_argument("--weight_decay", type=float, default=0.0) + t.add_argument("--warmup_steps", type=int, default=0) + t.add_argument("--warmup_ratio", type=float, default=0.04) + t.add_argument("--max_grad_norm", type=float, default=1.0) + t.add_argument("--logging_steps", type=int, default=50) + t.add_argument("--save_steps", type=float, default=2500) + t.add_argument("--save_total_limit", type=int, default=None) + t.add_argument("--eval_steps", type=int, default=500) + t.add_argument("--save_strategy", type=str, default="steps") + t.add_argument("--eval_strategy", type=str, default="no") + t.add_argument("--lr_scheduler_type", type=str, default="cosine") + t.add_argument("--fp16", action="store_true") + t.add_argument("--bf16", action="store_true") + t.add_argument("--deepspeed", type=str, default=None) + t.add_argument("--report_to", type=str, default="none") + t.add_argument("--run_name", type=str, default=None) + t.add_argument("--training_time_test_length", type=int, default=7) + + # WandB + w = parser.add_argument_group("WandB Arguments") + w.add_argument("--wandb_project", type=str, default=None) + w.add_argument("--wandb_run_name", type=str, default=None) + + return parser.parse_args() + + +def _setup_wandb(args): + if args.report_to not in ("wandb", "all"): + return + if args.wandb_project: + os.environ["WANDB_PROJECT"] = args.wandb_project + run_name = args.wandb_run_name or args.run_name or os.environ.get("WANDB_RUN_NAME") + if run_name: + os.environ["WANDB_RUN_NAME"] = run_name + args.run_name = run_name + local_rank = int(os.environ.get("LOCAL_RANK", 0)) + if local_rank == 0: + try: + import wandb + wandb.init( + project=os.environ.get("WANDB_PROJECT", "angelslim-dflash"), + name=run_name, + resume="allow", + ) + except ImportError: + print("[WARNING] wandb not installed. Install via: pip install wandb") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def train(): + args = parse_args() + _setup_wandb(args) + + dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} + torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) + + # ------------------------------------------------------------------ + # 1. Draft model config + # ------------------------------------------------------------------ + rank0_print("Loading draft model config...") + draft_model_config = DraftModelConfig.from_file(args.draft_model_config_path) + draft_model_config.target_model_name_or_path = args.target_model_name_or_path + draft_model_config.embed_weight_key = args.embed_weight_key + draft_model_config.trust_remote_code = args.trust_remote_code + + # Override DFlash params from CLI if specified + if args.block_size is not None: + draft_model_config.block_size = args.block_size + if args.num_anchors is not None: + draft_model_config.num_anchors = args.num_anchors + if args.loss_decay_gamma is not None: + draft_model_config.loss_decay_gamma = args.loss_decay_gamma + if args.attention_backend is not None: + draft_model_config.attention_backend = args.attention_backend + draft_model_config._attn_implementation = args.attention_backend + if args.mask_token_id is not None: + dfc = getattr(draft_model_config, "dflash_config", None) or {} + dfc["mask_token_id"] = args.mask_token_id + draft_model_config.dflash_config = dfc + + # ------------------------------------------------------------------ + # 2. Draft model + # ------------------------------------------------------------------ + rank0_print("Loading draft model...") + draft_model = create_draft_model(draft_model_config) + rank0_print( + f"Draft model parameters: {sum(p.numel() for p in draft_model.parameters()):,}" + ) + + # ------------------------------------------------------------------ + # 3. Offline datasets + # ------------------------------------------------------------------ + rank0_print(f"Loading offline training data from: {args.train_hidden_path}") + train_dataset = OfflineDFlashDataset( + data_dir=args.train_hidden_path, + file_pattern="*.ckpt", + cache_in_memory=args.cache_in_memory, + ) + rank0_print(f"Training samples: {len(train_dataset)}") + + eval_dataset = None + if args.eval_hidden_path: + rank0_print(f"Loading offline eval data from: {args.eval_hidden_path}") + eval_dataset = OfflineDFlashDataset( + data_dir=args.eval_hidden_path, + file_pattern="*.ckpt", + cache_in_memory=args.cache_in_memory, + ) + rank0_print(f"Eval samples: {len(eval_dataset)}") + + data_collator = DataCollatorWithPadding() + + # ------------------------------------------------------------------ + # 4. TrainingArguments + # ------------------------------------------------------------------ + training_args = transformers.TrainingArguments( + output_dir=args.output_dir, + num_train_epochs=args.num_train_epochs, + per_device_train_batch_size=args.per_device_train_batch_size, + per_device_eval_batch_size=args.per_device_eval_batch_size, + gradient_accumulation_steps=args.gradient_accumulation_steps, + learning_rate=args.learning_rate, + weight_decay=args.weight_decay, + warmup_steps=args.warmup_steps, + warmup_ratio=args.warmup_ratio, + max_grad_norm=args.max_grad_norm, + optim=args.optim, + lr_scheduler_type=args.lr_scheduler_type, + fp16=args.fp16, + bf16=args.bf16, + eval_strategy=args.eval_strategy, + save_strategy=args.save_strategy, + save_steps=args.save_steps, + save_total_limit=args.save_total_limit, + eval_steps=args.eval_steps, + logging_steps=args.logging_steps, + report_to=args.report_to, + run_name=args.run_name, + deepspeed=args.deepspeed, + remove_unused_columns=False, + ) + + # ------------------------------------------------------------------ + # 5. Trainer -- use Eagle3TrainerFactory + # ------------------------------------------------------------------ + rank0_print("Initializing trainer...") + trainer = Eagle3TrainerFactory.create( + training_mode="offline", + modal_type="DFlash", + draft_model=draft_model, + target_model=None, # Not needed — hidden states are pre-computed + length=args.training_time_test_length, + draft_model_config=draft_model_config, + args=training_args, + train_dataset=train_dataset, + eval_dataset=eval_dataset, + data_collator=data_collator, + ) + + # ------------------------------------------------------------------ + # 6. Train + # ------------------------------------------------------------------ + output_dir = Path(training_args.output_dir) + if list(output_dir.glob("checkpoint-*")): + rank0_print("Resuming training from checkpoint...") + trainer.train(resume_from_checkpoint=True) + else: + rank0_print("Starting fresh training run...") + trainer.train() + + rank0_print("Training completed!") + + +if __name__ == "__main__": + train() diff --git a/tools/train_dflash_online.py b/tools/train_dflash_online.py new file mode 100755 index 00000000..7c6171bf --- /dev/null +++ b/tools/train_dflash_online.py @@ -0,0 +1,524 @@ +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DFlash Online Training Script. + +Based on train_eagle3_online.py but adapted for DFlash's block-parallel +cross-attention training approach. +""" + +import argparse +import os +from pathlib import Path + +import torch +import transformers + +from angelslim.compressor.speculative import ( + DatasetManager, + DraftModelConfig, + Eagle3TrainerFactory, + create_draft_model, + create_target_model, + get_supported_chat_template_type_strings, +) +from angelslim.utils import rank0_print + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Train DFlash online model") + + # Model arguments + model_group = parser.add_argument_group("Model Arguments") + model_group.add_argument( + "--modal_type", + type=str, + default="DFlash", + help="Modal type, should be DFlash for DFlash training", + ) + model_group.add_argument( + "--training_mode", + type=str, + default="online", + choices=["online"], + help="Training mode (only online is supported for DFlash)", + ) + model_group.add_argument( + "--target_model_name_or_path", + type=str, + default=None, + help="Path to target model", + ) + model_group.add_argument( + "--draft_model_config_path", + type=str, + default=None, + help="Path to draft model config", + ) + model_group.add_argument( + "--target_backend", + type=str, + default="hf", + choices=["hf"], + help="Target model backend: hf (HuggingFace Transformers)", + ) + model_group.add_argument( + "--torch_dtype", + type=str, + default="bfloat16", + choices=["float16", "bfloat16", "float32"], + help="Data type for model weights", + ) + model_group.add_argument( + "--trust_remote_code", + action="store_true", + default=True, + help="Whether to trust remote code when loading models", + ) + model_group.add_argument( + "--embed_weight_key", + type=str, + default="model.embed_tokens.weight", + help="Key for embedding weights in model config", + ) + + # DFlash-specific arguments + dflash_group = parser.add_argument_group("DFlash Arguments") + dflash_group.add_argument( + "--block_size", + type=int, + default=16, + help="Block size for DFlash parallel prediction", + ) + dflash_group.add_argument( + "--num_anchors", + type=int, + default=512, + help="Number of anchor positions per sequence", + ) + dflash_group.add_argument( + "--loss_decay_gamma", + type=float, + default=None, + help=( + "Gamma for exponential loss decay weighting. " + "Suggested: 7 for block_size=16, 5 for 10, 4 for 8. " + "None disables decay." + ), + ) + dflash_group.add_argument( + "--attention_backend", + type=str, + default="flex_attention", + choices=["eager", "sdpa", "flex_attention"], + help="Attention backend for draft model", + ) + dflash_group.add_argument( + "--mask_token_id", + type=int, + default=None, + help="MASK token ID. If not provided, uses config or auto-detect.", + ) + + # Data arguments + data_group = parser.add_argument_group("Data Arguments") + data_group.add_argument( + "--train_data_path", + type=str, + nargs="+", + required=True, + help="Path to training data file(s) (JSON format). Can specify multiple files.", + ) + data_group.add_argument( + "--eval_data_path", + type=str, + default=None, + help="Path to evaluation data file (JSON format)", + ) + data_group.add_argument( + "--chat_template_type", + type=str, + default="qwen3", + help=( + f"Chat template type for conversation formatting. " + f"Supported types: {', '.join(get_supported_chat_template_type_strings())}" + ), + ) + data_group.add_argument( + "--num_proc", + type=int, + default=16, + help="Number of processes for data preprocessing", + ) + data_group.add_argument( + "--sample_num", + type=int, + default=None, + help="Number of max samples for data preprocessing", + ) + data_group.add_argument( + "--shuffle_seed", type=int, default=42, help="Random seed for shuffling dataset" + ) + data_group.add_argument( + "--display", + action="store_true", + default=False, + help="Display data samples during preprocessing", + ) + + # Training arguments + training_group = parser.add_argument_group("Training Arguments") + training_group.add_argument( + "--output_dir", + type=str, + required=True, + help="Output directory for model checkpoints", + ) + training_group.add_argument( + "--optim", type=str, default="adamw_torch", help="Optimizer to use" + ) + training_group.add_argument( + "--training_time_test_length", + type=int, + default=1, + help="Not used for DFlash (kept for compatibility)", + ) + training_group.add_argument( + "--model_max_length", + type=int, + default=3072, + help="Maximum sequence length", + ) + training_group.add_argument( + "--per_device_train_batch_size", + type=int, + default=2, + help="Batch size per device during training", + ) + training_group.add_argument( + "--per_device_eval_batch_size", + type=int, + default=2, + help="Batch size per device during evaluation", + ) + training_group.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass", + ) + training_group.add_argument( + "--num_train_epochs", + type=int, + default=6, + help="Total number of training epochs to perform", + ) + training_group.add_argument( + "--learning_rate", type=float, default=6e-4, help="Initial learning rate" + ) + training_group.add_argument( + "--weight_decay", type=float, default=0.0, help="Weight decay to apply" + ) + training_group.add_argument( + "--max_grad_norm", + type=float, + default=1.0, + help="Maximum gradient norm for clipping", + ) + training_group.add_argument( + "--warmup_steps", type=int, default=0, help="Number of steps for warmup" + ) + training_group.add_argument( + "--warmup_ratio", type=float, default=0.04, help="Ratio of warmup steps" + ) + training_group.add_argument( + "--logging_steps", type=int, default=50, help="Log every X updates steps" + ) + training_group.add_argument( + "--save_steps", + type=float, + default=5000, + help="Save checkpoint every X updates steps", + ) + training_group.add_argument( + "--eval_steps", type=int, default=1000, help="Run evaluation every X steps" + ) + training_group.add_argument( + "--save_total_limit", + type=int, + default=None, + help="Limit the total amount of checkpoints", + ) + training_group.add_argument( + "--deepspeed", type=str, default=None, help="DeepSpeed config file" + ) + training_group.add_argument("--fp16", action="store_true", help="Whether to use fp16 training") + training_group.add_argument("--bf16", action="store_true", help="Whether to use bf16 training") + training_group.add_argument( + "--save_strategy", type=str, default="no", help="Save strategy for checkpoints" + ) + training_group.add_argument( + "--eval_strategy", type=str, default="no", help="Evaluation strategy" + ) + training_group.add_argument( + "--lr_scheduler_type", + type=str, + default="cosine", + help="Learning rate scheduler type", + ) + training_group.add_argument( + "--run_name", type=str, default=None, help="Run name for tracking" + ) + training_group.add_argument( + "--report_to", + type=str, + default="none", + help="The list of integrations to report the results and logs to (e.g. 'wandb')", + ) + + # WandB arguments (mirrors SpecForge's --wandb-project / --wandb-name) + wandb_group = parser.add_argument_group("WandB Arguments") + wandb_group.add_argument( + "--wandb_project", + type=str, + default=None, + help="WandB project name. Overrides WANDB_PROJECT env var if set.", + ) + wandb_group.add_argument( + "--wandb_run_name", + type=str, + default=None, + help="WandB run name. Overrides --run_name if both are set.", + ) + + return parser.parse_args() + + +def _setup_wandb(args) -> None: + """Set up WandB environment variables and initialize wandb run on rank 0. + + Mirrors the --wandb-project / --wandb-name pattern from SpecForge. + Priority: CLI args > env vars > defaults. + """ + if args.report_to not in ("wandb", "all"): + return + + # CLI args take priority over env vars + if args.wandb_project: + os.environ["WANDB_PROJECT"] = args.wandb_project + + # Resolve run name: --wandb_run_name > --run_name > env WANDB_RUN_NAME + run_name = ( + args.wandb_run_name + or args.run_name + or os.environ.get("WANDB_RUN_NAME") + ) + if run_name: + os.environ["WANDB_RUN_NAME"] = run_name + # Propagate back so TrainingArguments picks it up + args.run_name = run_name + + # Explicit wandb.init() on rank 0 so project/name are registered immediately + local_rank = int(os.environ.get("LOCAL_RANK", 0)) + if local_rank == 0: + try: + import wandb + + wandb.init( + project=os.environ.get("WANDB_PROJECT", "angelslim-dflash"), + name=run_name, + resume="allow", + ) + except ImportError: + print( + "[WARNING] wandb not installed. " + "Install via: pip install wandb" + ) + + +def train(): + args = parse_args() + _setup_wandb(args) + + # Parse torch dtype + dtype_mapping = { + "float16": torch.float16, + "bfloat16": torch.bfloat16, + "float32": torch.float32, + } + torch_dtype = dtype_mapping.get(args.torch_dtype, torch.bfloat16) + + rank0_print("Loading draft model config...") + draft_model_config = DraftModelConfig.from_file(args.draft_model_config_path) + target_model_type = getattr(draft_model_config, "target_model_type", None) + + # Inject DFlash-specific config into the draft model config + # so the trainer can access them + draft_model_config.target_model_name_or_path = args.target_model_name_or_path + draft_model_config.embed_weight_key = args.embed_weight_key + draft_model_config.trust_remote_code = args.trust_remote_code + + # Override DFlash params from CLI if specified + if args.block_size is not None: + draft_model_config.block_size = args.block_size + if args.num_anchors is not None: + draft_model_config.num_anchors = args.num_anchors + if args.loss_decay_gamma is not None: + draft_model_config.loss_decay_gamma = args.loss_decay_gamma + if args.attention_backend is not None: + draft_model_config.attention_backend = args.attention_backend + if args.mask_token_id is not None: + if not hasattr(draft_model_config, "dflash_config") or draft_model_config.dflash_config is None: + draft_model_config.dflash_config = {} + draft_model_config.dflash_config["mask_token_id"] = args.mask_token_id + + # Set attention implementation + draft_model_config._attn_implementation = args.attention_backend + + # Create target model with specified backend + rank0_print(f"Loading target model with {args.target_backend} backend...") + target_model = create_target_model( + backend=args.target_backend, + model_path=args.target_model_name_or_path, + modal_type="LLM", # DFlash uses standard LLM target model + torch_dtype=torch_dtype, + trust_remote_code=args.trust_remote_code, + target_model_type=target_model_type, + ) + rank0_print("Target model loaded successfully") + + # Configure target model to capture the right layers for DFlash + dflash_config = getattr(draft_model_config, "dflash_config", {}) or {} + target_layer_ids = dflash_config.get("target_layer_ids", None) + if target_layer_ids is not None: + # Set aux_hidden_states_layer_ids to match DFlash's target_layer_ids + draft_model_config.aux_hidden_states_layer_ids = target_layer_ids + rank0_print(f"DFlash target layer IDs: {target_layer_ids}") + + # Create draft model + rank0_print("Loading draft model...") + rank0_print(f"draft_model_config: {draft_model_config}") + draft_model = create_draft_model(draft_model_config) + rank0_print("Draft model loaded successfully") + rank0_print( + f"Draft model parameters: {sum(p.numel() for p in draft_model.parameters()):,}" + ) + + # Create datasets using DatasetManager + rank0_print( + "Creating training and evaluation datasets " + f"with chat template type: {args.chat_template_type}..." + ) + # DatasetBuilderFactory doesn't know "DFlash"; DFlash uses the same data + # format as "LLM", so temporarily override modal_type for dataset creation. + args.modal_type = "LLM" + dataset_manager = DatasetManager( + data_args=args, + tokenizer=target_model.tokenizer, + model_max_length=args.model_max_length, + chat_template_type=args.chat_template_type, + display=args.display, + target_model_type=target_model_type, + ) + args.modal_type = "DFlash" # restore for trainer factory + train_dataset, eval_dataset, data_collator = dataset_manager.create_online_datasets() + rank0_print( + f"Train dataset size: {len(train_dataset)}, " + f"Eval dataset size: {len(eval_dataset) if eval_dataset else 0}" + ) + + # Create TrainingArguments + basic_args = { + "output_dir": args.output_dir, + "num_train_epochs": args.num_train_epochs, + } + + batch_args = { + "per_device_train_batch_size": args.per_device_train_batch_size, + "per_device_eval_batch_size": args.per_device_eval_batch_size, + "gradient_accumulation_steps": args.gradient_accumulation_steps, + "remove_unused_columns": False, + } + + optimizer_args = { + "learning_rate": args.learning_rate, + "weight_decay": args.weight_decay, + "warmup_steps": args.warmup_steps, + "warmup_ratio": args.warmup_ratio, + "optim": args.optim, + "lr_scheduler_type": args.lr_scheduler_type, + "max_grad_norm": args.max_grad_norm, + } + + precision_args = { + "fp16": args.fp16, + "bf16": args.bf16, + } + + checkpoint_args = { + "eval_strategy": args.eval_strategy, + "save_strategy": args.save_strategy, + "save_steps": args.save_steps, + "save_total_limit": args.save_total_limit, + } + + logging_args = { + "logging_steps": args.logging_steps, + "eval_steps": args.eval_steps, + "report_to": args.report_to, + "run_name": args.run_name, + } + + distributed_args = { + "deepspeed": args.deepspeed, + } + + training_args = transformers.TrainingArguments( + **basic_args, + **batch_args, + **optimizer_args, + **precision_args, + **checkpoint_args, + **logging_args, + **distributed_args, + ) + + # Initialize trainer + rank0_print("Initializing DFlash trainer...") + trainer = Eagle3TrainerFactory.create( + training_mode=args.training_mode, + modal_type=args.modal_type, + draft_model=draft_model, + target_model=target_model, + length=args.training_time_test_length, + draft_model_config=draft_model_config, + args=training_args, + train_dataset=train_dataset, + eval_dataset=eval_dataset, + data_collator=data_collator, + ) + + # Start training + if list(Path(training_args.output_dir).glob("checkpoint-*")): + rank0_print("Resuming training from checkpoint...") + trainer.train(resume_from_checkpoint=True) + else: + rank0_print("Starting DFlash training...") + trainer.train() + rank0_print("DFlash training completed!") + + +if __name__ == "__main__": + train() From a68d3a3b80d01e8d8eb97072fee493e92b014c5b Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:42:46 +0800 Subject: [PATCH 02/23] Refactor bytes_to_unicode import in tiktoken_tokenizer Removed the inline implementation of bytes_to_unicode and imported it directly from the transformers library. --- angelslim/models/llm/tiktoken_tokenizer.py | 23 +++------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/angelslim/models/llm/tiktoken_tokenizer.py b/angelslim/models/llm/tiktoken_tokenizer.py index 63691c22..8c37ef92 100644 --- a/angelslim/models/llm/tiktoken_tokenizer.py +++ b/angelslim/models/llm/tiktoken_tokenizer.py @@ -8,26 +8,9 @@ import tiktoken from tiktoken.load import load_tiktoken_bpe from tokenizers import AddedToken -try: - from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode -except ImportError: - # bytes_to_unicode was removed from newer transformers versions. - # Inline the original GPT-2 implementation. - def bytes_to_unicode(): - bs = ( - list(range(ord("!"), ord("~") + 1)) - + list(range(ord("¡"), ord("¬") + 1)) - + list(range(ord("®"), ord("ÿ") + 1)) - ) - cs = bs[:] - n = 0 - for b in range(2**8): - if b not in bs: - bs.append(b) - cs.append(2**8 + n) - n += 1 - cs = [chr(c) for c in cs] - return dict(zip(bs, cs)) + +from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode + from transformers.tokenization_utils import PreTrainedTokenizer logger = getLogger(__name__) From 97d236d1eaaa205e5665fcb480addf40f537f06b Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:29:38 +0800 Subject: [PATCH 03/23] change import --- angelslim/compressor/quant/ptq.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/angelslim/compressor/quant/ptq.py b/angelslim/compressor/quant/ptq.py index 02e5cc4a..fcb3dd70 100644 --- a/angelslim/compressor/quant/ptq.py +++ b/angelslim/compressor/quant/ptq.py @@ -18,10 +18,8 @@ import torch from safetensors.torch import load_file -try: - from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts -except ImportError: - Qwen3VLMoeTextExperts = None # not available in this transformers version + +from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts from ...utils import find_parent_layer_and_sub_name, print_info From 8030a62e8b9dd13abc505a5dd80d0f4a52e8bf78 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Fri, 13 Mar 2026 22:48:31 +0800 Subject: [PATCH 04/23] change import --- .gitignore | 1 + angelslim/compressor/quant/ptq.py | 8 +++----- .../inference/models/eagle3/target/modeling_qwen2_kv.py | 9 +-------- .../inference/models/eagle3/target/modeling_qwen3_kv.py | 9 +-------- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 393a1409..d68ab077 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist/ eval/ *_ckpt*/ output/ +outputs/ outs/ wandb/ tools/results/ diff --git a/angelslim/compressor/quant/ptq.py b/angelslim/compressor/quant/ptq.py index 02e5cc4a..85bb550d 100644 --- a/angelslim/compressor/quant/ptq.py +++ b/angelslim/compressor/quant/ptq.py @@ -18,10 +18,8 @@ import torch from safetensors.torch import load_file -try: - from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts -except ImportError: - Qwen3VLMoeTextExperts = None # not available in this transformers version + +from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts from ...utils import find_parent_layer_and_sub_name, print_info @@ -286,7 +284,7 @@ def _convert(self): # For qwen3_vl_moe models, we need to insert MoEQDQModule for MOE experts, # since these modules contain gate_up_proj and down_proj, which are defined as # nn.Parameters, not nn.Linear. - if Qwen3VLMoeTextExperts is not None and Qwen3VLMoeTextExperts in self.quant_model.observer_layer_classes: + if Qwen3VLMoeTextExperts in self.quant_model.observer_layer_classes: for name, sub_layer in self.quant_model.model.named_modules(): parent_layer, sub_name = find_parent_layer_and_sub_name(quant_convert_module, name) moe_qdq_module = self.quant_model.get_moe_qdq_module(sub_layer, name) diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py index 1d7cf5bc..3c2d5a71 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py @@ -15,14 +15,7 @@ import torch from torch import nn from transformers.activations import ACT2FN -from transformers.cache_utils import Cache, StaticCache -try: - from transformers.cache_utils import SlidingWindowCache -except ImportError: - # SlidingWindowCache was removed/renamed in newer transformers versions. - # Define a stub so isinstance() checks below still work (they'll just never match). - class SlidingWindowCache: # type: ignore[no-redef] - pass +from transformers.cache_utils import Cache, SlidingWindowCache, StaticCache from transformers.generation import GenerationMixin from transformers.modeling_attn_mask_utils import AttentionMaskConverter from transformers.modeling_flash_attention_utils import FlashAttentionKwargs diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py index e3e41904..a617e84c 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen3_kv.py @@ -15,14 +15,7 @@ import torch from torch import nn from transformers.activations import ACT2FN -from transformers.cache_utils import Cache, StaticCache -try: - from transformers.cache_utils import SlidingWindowCache -except ImportError: - # SlidingWindowCache was removed/renamed in newer transformers versions. - # Define a stub so isinstance() checks below still work (they'll just never match). - class SlidingWindowCache: # type: ignore[no-redef] - pass +from transformers.cache_utils import Cache, SlidingWindowCache, StaticCache from transformers.generation import GenerationMixin from transformers.modeling_attn_mask_utils import AttentionMaskConverter from transformers.modeling_flash_attention_utils import FlashAttentionKwargs From f8e93ff37e033f89d2233b5103b4029f520502df Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:08:16 +0800 Subject: [PATCH 05/23] change import --- angelslim/compressor/quant/ptq.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/angelslim/compressor/quant/ptq.py b/angelslim/compressor/quant/ptq.py index 85bb550d..cca48b3d 100644 --- a/angelslim/compressor/quant/ptq.py +++ b/angelslim/compressor/quant/ptq.py @@ -18,10 +18,8 @@ import torch from safetensors.torch import load_file - from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts - from ...utils import find_parent_layer_and_sub_name, print_info from ..compressor_factory import CompressorFactory from .core import PTQHook @@ -284,7 +282,7 @@ def _convert(self): # For qwen3_vl_moe models, we need to insert MoEQDQModule for MOE experts, # since these modules contain gate_up_proj and down_proj, which are defined as # nn.Parameters, not nn.Linear. - if Qwen3VLMoeTextExperts in self.quant_model.observer_layer_classes: + if Qwen3VLMoeTextExperts in self.quant_model.observer_layer_classes: for name, sub_layer in self.quant_model.model.named_modules(): parent_layer, sub_name = find_parent_layer_and_sub_name(quant_convert_module, name) moe_qdq_module = self.quant_model.get_moe_qdq_module(sub_layer, name) From 9ad25b86e7cabd9641f7bde41ba1af68aa90d36d Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:10:42 +0800 Subject: [PATCH 06/23] change import --- angelslim/models/vlm/qwen3_vl.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/angelslim/models/vlm/qwen3_vl.py b/angelslim/models/vlm/qwen3_vl.py index 52efa925..183db3ed 100644 --- a/angelslim/models/vlm/qwen3_vl.py +++ b/angelslim/models/vlm/qwen3_vl.py @@ -15,12 +15,7 @@ import torch import torch.nn.functional as F from tqdm import tqdm -from transformers import AutoProcessor, AutoTokenizer - -try: - from transformers import Qwen3VLForConditionalGeneration -except ImportError: - Qwen3VLForConditionalGeneration = None # not available in this transformers version +from transformers import AutoProcessor, AutoTokenizer, Qwen3VLForConditionalGeneration from ...compressor.quant.core import LossFilter, PTQVLMSaveVllmHF from ...utils import find_layers, print_info From a816fbeb30ebf0ebf54e1114a9c78ab294a5341d Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:12:13 +0800 Subject: [PATCH 07/23] Refactor imports for Qwen3VLMoe model Removed try-except block for Qwen3VLMoe imports. --- angelslim/models/vlm/qwen3_vl_moe.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/angelslim/models/vlm/qwen3_vl_moe.py b/angelslim/models/vlm/qwen3_vl_moe.py index 37fc085d..30607dc6 100644 --- a/angelslim/models/vlm/qwen3_vl_moe.py +++ b/angelslim/models/vlm/qwen3_vl_moe.py @@ -21,14 +21,10 @@ from transformers import ( AutoProcessor, AutoTokenizer, + Qwen3VLMoeForConditionalGeneration, ) +from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts -try: - from transformers import Qwen3VLMoeForConditionalGeneration - from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts -except ImportError: - Qwen3VLMoeForConditionalGeneration = None # not available in this transformers version - Qwen3VLMoeTextExperts = None from angelslim.compressor.quant.core.quant_func import get_fp_maxval from angelslim.compressor.quant.observers import MoEAbsmaxPertensorObserver From ac47f75c7d7bc723789fc72c176458067ddd9d3d Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:12:55 +0800 Subject: [PATCH 08/23] change import --- angelslim/models/vlm/qwen3_vl_moe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/angelslim/models/vlm/qwen3_vl_moe.py b/angelslim/models/vlm/qwen3_vl_moe.py index 30607dc6..f3f311c2 100644 --- a/angelslim/models/vlm/qwen3_vl_moe.py +++ b/angelslim/models/vlm/qwen3_vl_moe.py @@ -25,7 +25,6 @@ ) from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts - from angelslim.compressor.quant.core.quant_func import get_fp_maxval from angelslim.compressor.quant.observers import MoEAbsmaxPertensorObserver From 472b5c4f6fa8225b06165e0b185ca479a6566bb4 Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:14:09 +0800 Subject: [PATCH 09/23] change import --- angelslim/models/omni/qwen3_omni.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/angelslim/models/omni/qwen3_omni.py b/angelslim/models/omni/qwen3_omni.py index 20886134..4dbdf933 100644 --- a/angelslim/models/omni/qwen3_omni.py +++ b/angelslim/models/omni/qwen3_omni.py @@ -17,13 +17,9 @@ from transformers import ( AutoProcessor, AutoTokenizer, + Qwen3OmniMoeForConditionalGeneration, ) -try: - from transformers import Qwen3OmniMoeForConditionalGeneration -except ImportError: - Qwen3OmniMoeForConditionalGeneration = None # not available in this transformers version - from ...compressor.quant.core import PTQVLMSaveVllmHF from ...utils import find_layers, print_info from ..base_model import BaseLLMModel From 45387e955c6da42449f2900d4abcf49f44355bf5 Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:14:42 +0800 Subject: [PATCH 10/23] Remove unused imports in tiktoken_tokenizer.py --- angelslim/models/llm/tiktoken_tokenizer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/angelslim/models/llm/tiktoken_tokenizer.py b/angelslim/models/llm/tiktoken_tokenizer.py index 8c37ef92..243a5b23 100644 --- a/angelslim/models/llm/tiktoken_tokenizer.py +++ b/angelslim/models/llm/tiktoken_tokenizer.py @@ -8,9 +8,7 @@ import tiktoken from tiktoken.load import load_tiktoken_bpe from tokenizers import AddedToken - from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode - from transformers.tokenization_utils import PreTrainedTokenizer logger = getLogger(__name__) From ebc8f6bf49fdb2fe261c75069456610aef4274af Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 16 Mar 2026 20:49:37 +0800 Subject: [PATCH 11/23] dev_dflash --- .../dataset_builder/base_dataset_builder.py | 16 +++- .../train/models/draft/qwen_dflash.py | 63 ++++--------- .../speculative/train/trainer/__init__.py | 2 +- .../train/trainer/offline_dflash_trainer.py | 6 +- .../train/trainer/online_dflash_trainer.py | 94 +++++++------------ tools/generate_dflash_data.py | 86 ++++++++++------- tools/train_dflash_offline.py | 70 ++++++++------ tools/train_dflash_online.py | 24 ++--- 8 files changed, 170 insertions(+), 191 deletions(-) diff --git a/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py b/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py index 72437a05..d7fd2f5e 100644 --- a/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py +++ b/angelslim/compressor/speculative/train/data/dataset_builder/base_dataset_builder.py @@ -29,7 +29,12 @@ class DatasetBuilder(metaclass=ABCMeta): @abstractmethod def build_dataset( - self, datapath: str, num_proc: int = 8, shuffle: bool = True, min_loss_tokens: Optional[int] = None, **kwargs + self, + datapath: str, + num_proc: int = 8, + shuffle: bool = True, + min_loss_tokens: Optional[int] = None, + **kwargs, ) -> Dataset: pass @@ -162,15 +167,18 @@ def build_dataset( num_proc=num_proc, desc="Filtering empty input_ids", ) - + if min_loss_tokens is not None: processed_ds = processed_ds.filter( - lambda batch: [sum(sum(x) if isinstance(x, list) else x for x in m) >= min_loss_tokens for m in batch["loss_mask"]], + lambda batch: [ + sum(sum(x) if isinstance(x, list) else x for x in m) >= min_loss_tokens + for m in batch["loss_mask"] + ], batched=True, num_proc=num_proc, desc=f"Filtering sequences with loss tokens < {min_loss_tokens}", ) - + processed_ds.set_format(type="torch") return processed_ds diff --git a/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py b/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py index b33e1649..faf00343 100755 --- a/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py +++ b/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py @@ -25,7 +25,6 @@ from torch import nn from transformers import DynamicCache from transformers.cache_utils import Cache -from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.qwen3.modeling_qwen3 import ( ALL_ATTENTION_FUNCTIONS, FlashAttentionKwargs, @@ -62,9 +61,7 @@ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): return q_embed, k_embed -def build_target_layer_ids( - num_target_layers: int, num_draft_layers: int -) -> List[int]: +def build_target_layer_ids(num_target_layers: int, num_draft_layers: int) -> List[int]: """Compute target layer IDs to capture from the target model.""" if num_draft_layers == 1: return [(num_target_layers // 2)] @@ -72,8 +69,7 @@ def build_target_layer_ids( end = num_target_layers - 3 span = end - start target_layer_ids = [ - int(round(start + (i * span) / (num_draft_layers - 1))) - for i in range(num_draft_layers) + int(round(start + (i * span) / (num_draft_layers - 1))) for i in range(num_draft_layers) ] return target_layer_ids @@ -105,9 +101,7 @@ def __init__(self, config: Qwen3Config, layer_idx: int): self.head_dim = getattr( config, "head_dim", config.hidden_size // config.num_attention_heads ) - self.num_key_value_groups = ( - config.num_attention_heads // config.num_key_value_heads - ) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads self.scaling = self.head_dim**-0.5 self.attention_dropout = config.attention_dropout self.is_causal = False @@ -134,9 +128,7 @@ def __init__(self, config: Qwen3Config, layer_idx: int): self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) self.sliding_window = ( - config.sliding_window - if config.layer_types[layer_idx] == "sliding_attention" - else None + config.sliding_window if config.layer_types[layer_idx] == "sliding_attention" else None ) def forward( @@ -158,12 +150,8 @@ def forward( k_noise = self.k_proj(hidden_states) v_ctx = self.v_proj(target_hidden) v_noise = self.v_proj(hidden_states) - k = torch.cat([k_ctx, k_noise], dim=1).view( - bsz, ctx_len + q_len, -1, self.head_dim - ) - v = torch.cat([v_ctx, v_noise], dim=1).view( - bsz, ctx_len + q_len, -1, self.head_dim - ) + k = torch.cat([k_ctx, k_noise], dim=1).view(bsz, ctx_len + q_len, -1, self.head_dim) + v = torch.cat([v_ctx, v_noise], dim=1).view(bsz, ctx_len + q_len, -1, self.head_dim) k = self.k_norm(k).transpose(1, 2) v = v.transpose(1, 2) cos, sin = position_embeddings @@ -199,9 +187,7 @@ def __init__(self, config: Qwen3Config, layer_idx: int): self.self_attn = Qwen3DFlashAttention(config=config, layer_idx=layer_idx) self.mlp = Qwen3MLP(config) self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) - self.post_attention_layernorm = Qwen3RMSNorm( - config.hidden_size, eps=config.rms_norm_eps - ) + self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) def forward( self, @@ -213,9 +199,7 @@ def forward( output_attentions: Optional[bool] = False, use_cache: Optional[bool] = False, cache_position: Optional[torch.LongTensor] = None, - position_embeddings: Optional[ - Tuple[torch.Tensor, torch.Tensor] - ] = None, + position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, **kwargs: Unpack[FlashAttentionKwargs], ) -> torch.FloatTensor: residual = hidden_states @@ -324,9 +308,7 @@ def spec_generate( dtype=torch.long, device=target.device, ) - position_ids = torch.arange( - output_ids.shape[1], device=target.device - ).unsqueeze(0) + position_ids = torch.arange(output_ids.shape[1], device=target.device).unsqueeze(0) past_key_values_target = DynamicCache() past_key_values_draft = DynamicCache() @@ -342,12 +324,8 @@ def spec_generate( ) output_ids[:, :num_input_tokens] = input_ids - output_ids[:, num_input_tokens : num_input_tokens + 1] = sample( - output.logits, temperature - ) - target_hidden = extract_context_feature( - output.hidden_states, self.target_layer_ids - ) + output_ids[:, num_input_tokens : num_input_tokens + 1] = sample(output.logits, temperature) + target_hidden = extract_context_feature(output.hidden_states, self.target_layer_ids) # Decode stage acceptance_lengths = [] @@ -381,22 +359,17 @@ def spec_generate( posterior = sample(output.logits, temperature) acceptance_length = ( - (block_output_ids[:, 1:] == posterior[:, :-1]) - .cumprod(dim=1) - .sum(dim=1)[0] - .item() + (block_output_ids[:, 1:] == posterior[:, :-1]).cumprod(dim=1).sum(dim=1)[0].item() ) output_ids[:, start : start + acceptance_length + 1] = block_output_ids[ :, : acceptance_length + 1 ] - output_ids[:, start + acceptance_length + 1] = posterior[ - :, acceptance_length - ] + output_ids[:, start + acceptance_length + 1] = posterior[:, acceptance_length] start += acceptance_length + 1 past_key_values_target.crop(start) - target_hidden = extract_context_feature( - output.hidden_states, self.target_layer_ids - )[:, : acceptance_length + 1, :] + target_hidden = extract_context_feature(output.hidden_states, self.target_layer_ids)[ + :, : acceptance_length + 1, : + ] acceptance_lengths.append(acceptance_length + 1) if stop_token_ids is not None and any( stop_token_id in output_ids[:, num_input_tokens:] @@ -411,8 +384,6 @@ def spec_generate( output_ids[0][num_input_tokens:], stop_token_ids ).nonzero(as_tuple=True)[0] if stop_token_indices.numel() > 0: - output_ids = output_ids[ - :, : num_input_tokens + stop_token_indices[0] + 1 - ] + output_ids = output_ids[:, : num_input_tokens + stop_token_indices[0] + 1] return output_ids diff --git a/angelslim/compressor/speculative/train/trainer/__init__.py b/angelslim/compressor/speculative/train/trainer/__init__.py index 62bc7970..cd4db79c 100644 --- a/angelslim/compressor/speculative/train/trainer/__init__.py +++ b/angelslim/compressor/speculative/train/trainer/__init__.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .offline_dflash_trainer import OfflineDFlashTrainer from .offline_eagle3_trainer import OfflineEagle3Trainer, OfflineVLMEagle3Trainer from .online_dflash_trainer import OnlineDFlashTrainer -from .offline_dflash_trainer import OfflineDFlashTrainer from .online_eagle3_trainer import ( OnlineEagle3Trainer, OnlineTTSEagle3Trainer, diff --git a/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py b/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py index e08c6a70..b58b74cf 100644 --- a/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py +++ b/angelslim/compressor/speculative/train/trainer/offline_dflash_trainer.py @@ -37,8 +37,8 @@ def prepare_data_for_draft_model(self, inputs): attention_mask [B, S] """ return { - "input_ids": inputs["input_ids"], - "hidden_states": inputs["hidden_states"], - "loss_mask": inputs["loss_mask"], + "input_ids": inputs["input_ids"], + "hidden_states": inputs["hidden_states"], + "loss_mask": inputs["loss_mask"], "attention_mask": inputs["attention_mask"], } diff --git a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py index 4040daa4..e2ff3741 100755 --- a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py +++ b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py @@ -27,8 +27,8 @@ import torch import torch.nn.functional as F -from torch import nn from safetensors import safe_open +from torch import nn from transformers import AutoConfig from .eagle3_trainer import Eagle3Trainer @@ -36,6 +36,7 @@ try: from torch.nn.attention.flex_attention import BlockMask, create_block_mask + FLEX_ATTENTION_AVAILABLE = True except ImportError: FLEX_ATTENTION_AVAILABLE = False @@ -98,7 +99,8 @@ def __init__(self, config): self.config = config self.embed_tokens = nn.Embedding( - config.vocab_size, config.hidden_size, + config.vocab_size, + config.hidden_size, padding_idx=getattr(config, "pad_token_id", None), ) @@ -115,9 +117,7 @@ def from_pretrained( trust_remote_code: bool = False, ) -> "TargetEmbeddingsAndHead": - config = AutoConfig.from_pretrained( - model_path, trust_remote_code=trust_remote_code - ) + config = AutoConfig.from_pretrained(model_path, trust_remote_code=trust_remote_code) instance = cls(config) if embed_key is None: @@ -134,9 +134,7 @@ def from_pretrained( return instance - def _load_weights( - self, model_path: str, embed_key: str, lm_head_key: str, tie_weights: bool - ): + def _load_weights(self, model_path: str, embed_key: str, lm_head_key: str, tie_weights: bool): index_files = glob.glob(os.path.join(model_path, "*.index.json")) files_to_load = {} @@ -148,9 +146,7 @@ def _load_weights( if embed_key in weight_map: files_to_load[embed_key] = weight_map[embed_key] else: - raise ValueError( - f"Embedding key '{embed_key}' not found in weight map." - ) + raise ValueError(f"Embedding key '{embed_key}' not found in weight map.") if not tie_weights: if lm_head_key in weight_map: @@ -248,9 +244,7 @@ def __init__( self.block_size = getattr(draft_model_config, "block_size", 16) self.num_anchors = getattr(draft_model_config, "num_anchors", 512) self.loss_decay_gamma = getattr(draft_model_config, "loss_decay_gamma", None) - self.attention_backend = getattr( - draft_model_config, "attention_backend", "flex_attention" - ) + self.attention_backend = getattr(draft_model_config, "attention_backend", "flex_attention") self.mask_token_id = dflash_config.get( "mask_token_id", getattr(draft_model_config, "mask_token_id", None), @@ -262,9 +256,7 @@ def __init__( if target_model is not None: target_model_path = getattr(target_model, "model_path", None) if target_model_path is None: - target_model_path = getattr( - draft_model_config, "target_model_name_or_path", None - ) + target_model_path = getattr(draft_model_config, "target_model_name_or_path", None) embed_weight_key = getattr( draft_model_config, "embed_weight_key", "model.embed_tokens.weight" ) @@ -333,12 +325,8 @@ def _sample_anchor_positions( max_n = min(self.num_anchors, max_valid - 1) - indices = ( - torch.arange(max_anchor + 1, device=device).unsqueeze(0).expand(bsz, -1) - ) - masked_indices = torch.where( - valid, indices, torch.tensor(seq_len + 1, device=device) - ) + indices = torch.arange(max_anchor + 1, device=device).unsqueeze(0).expand(bsz, -1) + masked_indices = torch.where(valid, indices, torch.tensor(seq_len + 1, device=device)) random_vals = torch.rand(bsz, max_anchor + 1, device=device) random_vals = torch.where(valid, random_vals, torch.tensor(2.0, device=device)) @@ -347,12 +335,10 @@ def _sample_anchor_positions( gathered = torch.gather(masked_indices, 1, sorted_idx) anchors = gathered[:, :max_n].sort(dim=1).values - keep_mask = torch.arange(max_n, device=device).unsqueeze( - 0 - ) < valid_counts.unsqueeze(1).clamp(max=max_n) - anchors = torch.where( - keep_mask, anchors, torch.tensor(0, dtype=torch.long, device=device) - ) + keep_mask = torch.arange(max_n, device=device).unsqueeze(0) < valid_counts.unsqueeze( + 1 + ).clamp(max=max_n) + anchors = torch.where(keep_mask, anchors, torch.tensor(0, dtype=torch.long, device=device)) return anchors, keep_mask @@ -370,9 +356,7 @@ def _create_noise_embed(self, input_ids, anchor_positions, block_keep_mask): bs = self.block_size device = input_ids.device - noise_ids = torch.full( - (bsz, n * bs), self.mask_token_id, dtype=torch.long, device=device - ) + noise_ids = torch.full((bsz, n * bs), self.mask_token_id, dtype=torch.long, device=device) block_starts = torch.arange(n, device=device) * bs block_starts = block_starts.unsqueeze(0).expand(bsz, -1) @@ -419,22 +403,14 @@ def _compute_dflash_loss_and_accuracy( # No valid anchors → return zero loss connected to model params (DDP-safe) if anchor_positions is None: - zero_loss = sum( - p.sum() * 0.0 - for p in model.parameters() - if p.requires_grad - ) + zero_loss = sum(p.sum() * 0.0 for p in model.parameters() if p.requires_grad) return zero_loss, torch.tensor(0.0, device=device) # ── 2. Noise embedding ──────────────────────────────────────────────── - noise_embedding = self._create_noise_embed( - input_ids, anchor_positions, block_keep_mask - ) + noise_embedding = self._create_noise_embed(input_ids, anchor_positions, block_keep_mask) # ── 3. Position IDs [B, S + N*block_size] ─────────────────────────── - context_position_ids = ( - torch.arange(seq_len, device=device).unsqueeze(0).expand(bsz, -1) - ) + context_position_ids = torch.arange(seq_len, device=device).unsqueeze(0).expand(bsz, -1) draft_position_ids = self._create_position_ids(anchor_positions) full_position_ids = torch.cat([context_position_ids, draft_position_ids], dim=1) @@ -458,7 +434,7 @@ def _compute_dflash_loss_and_accuracy( attention_mask=dflash_attn_mask, position_ids=full_position_ids, ) - + output_hidden = output_hidden.to(self.target_lm_head.weight.dtype) logits = self.target_lm_head(output_hidden) @@ -476,13 +452,11 @@ def _compute_dflash_loss_and_accuracy( ) # [B, N, bs] # ── 7. Weight mask: valid block × in-bounds × skip anchor × loss_mask ─ - weight_mask = ( - block_keep_mask.unsqueeze(-1).expand(-1, -1, bs).float() - ) + weight_mask = block_keep_mask.unsqueeze(-1).expand(-1, -1, bs).float() weight_mask = weight_mask * valid_label_mask.float() pos_in_block = torch.arange(bs, device=device).view(1, 1, -1) - weight_mask = weight_mask * (pos_in_block > 0).float() # skip pos 0 (anchor) + weight_mask = weight_mask * (pos_in_block > 0).float() # skip pos 0 (anchor) gathered_loss_mask = torch.gather( loss_mask.unsqueeze(1).expand(-1, anchor_positions.size(1), -1), @@ -491,7 +465,7 @@ def _compute_dflash_loss_and_accuracy( ) weight_mask = weight_mask * gathered_loss_mask - binary_eval_mask = weight_mask.view(-1) # no decay, used for accuracy + binary_eval_mask = weight_mask.view(-1) # no decay, used for accuracy # ── 8. Exponential decay: exp(-(k-1)/γ), k=1 gets weight 1.0 ───────── if self.loss_decay_gamma is not None and self.loss_decay_gamma > 0: @@ -500,7 +474,7 @@ def _compute_dflash_loss_and_accuracy( weight_mask = weight_mask * decay # ── 9. Cross-entropy loss ───────────────────────────────────────────── - flat_logits = logits.view(-1, logits.size(-1)) + flat_logits = logits.view(-1, logits.size(-1)) flat_targets = target_ids.view(-1) flat_weights = weight_mask.view(-1) @@ -536,10 +510,12 @@ def compute_loss( loss_mask=data["loss_mask"], ) - self.log({ - "train/loss": round(float(loss.item()), 4), - "train/accuracy": round(float(accuracy.item()), 4), - }) + self.log( + { + "train/loss": round(float(loss.item()), 4), + "train/accuracy": round(float(accuracy.item()), 4), + } + ) return loss @@ -561,9 +537,11 @@ def prediction_step( loss_mask=data["loss_mask"], ) - self.log({ - "eval/loss": round(float(loss.item()), 4), - "eval/accuracy": round(float(accuracy.item()), 4), - }) + self.log( + { + "eval/loss": round(float(loss.item()), 4), + "eval/accuracy": round(float(accuracy.item()), 4), + } + ) return loss, None, None diff --git a/tools/generate_dflash_data.py b/tools/generate_dflash_data.py index b4d1e345..b141f024 100755 --- a/tools/generate_dflash_data.py +++ b/tools/generate_dflash_data.py @@ -45,36 +45,56 @@ def parse_args(): parser.add_argument("--target_model_name_or_path", type=str, required=True) parser.add_argument("--draft_model_config_path", type=str, required=True) parser.add_argument( - "--target_backend", type=str, default="hf", choices=["hf"], + "--target_backend", + type=str, + default="hf", + choices=["hf"], help="Target model backend", ) parser.add_argument( - "--torch_dtype", type=str, default="bfloat16", + "--torch_dtype", + type=str, + default="bfloat16", choices=["float16", "bfloat16", "float32"], ) parser.add_argument("--trust_remote_code", action="store_true", default=True) # Data - parser.add_argument("--train_data_path", type=str, nargs="+", required=True, - help="Input JSONL file(s)") - parser.add_argument("--output_dir", type=str, required=True, - help="Directory to save .ckpt files") parser.add_argument( - "--chat_template_type", type=str, default="qwen3", + "--train_data_path", type=str, nargs="+", required=True, help="Input JSONL file(s)" + ) + parser.add_argument( + "--output_dir", type=str, required=True, help="Directory to save .ckpt files" + ) + parser.add_argument( + "--chat_template_type", + type=str, + default="qwen3", help=f"Supported: {', '.join(get_supported_chat_template_type_strings())}", ) parser.add_argument("--model_max_length", type=int, default=3072) - parser.add_argument("--block_size", type=int, default=16, - help="Block size for DFlash parallel prediction") - parser.add_argument("--num_proc", type=int, default=16, - help="Workers for tokenization (dataset.map)") - parser.add_argument("--batch_size", type=int, default=1, - help="Samples per forward pass (keep at 1 for variable-length seqs)") + parser.add_argument( + "--block_size", type=int, default=16, help="Block size for DFlash parallel prediction" + ) + parser.add_argument( + "--num_proc", type=int, default=16, help="Workers for tokenization (dataset.map)" + ) + parser.add_argument( + "--batch_size", + type=int, + default=1, + help="Samples per forward pass (keep at 1 for variable-length seqs)", + ) parser.add_argument("--shuffle_seed", type=int, default=42) - parser.add_argument("--sample_num", type=int, default=None, - help="Limit number of samples (for debugging)") - parser.add_argument("--shard_size", type=int, default=0, - help="Save a new sub-directory every N files (0 = no sharding)") + parser.add_argument( + "--sample_num", type=int, default=None, help="Limit number of samples (for debugging)" + ) + parser.add_argument( + "--shard_size", + type=int, + default=0, + help="Save a new sub-directory every N files (0 = no sharding)", + ) return parser.parse_args() @@ -141,10 +161,11 @@ def main(): # -------------------------------------------------------------------------- rank0_print("Building dataset...") # Temporarily patch args so DatasetManager picks the correct builder - args.modal_type = "LLM" # DFlash uses the LLM tokenisation path + args.modal_type = "LLM" # DFlash uses the LLM tokenisation path args.training_mode = "online" # We want the text→token builder, not offline .ckpt loader from transformers import AutoTokenizer + tokenizer = AutoTokenizer.from_pretrained( args.target_model_name_or_path, trust_remote_code=True ) @@ -155,16 +176,16 @@ def main(): model_max_length=args.model_max_length, chat_template_type=args.chat_template_type, ) - + # Restore modal_type to DFlash so DFlash-specific filtering (min_loss_tokens) applies args.modal_type = "DFlash" - + ( - _, # offline_train_dataset (unused here) - _, # offline_eval_dataset + _, # offline_train_dataset (unused here) + _, # offline_eval_dataset online_train_dataset, - _, # online_eval_dataset - _, # data_collator + _, # online_eval_dataset + _, # data_collator ) = dataset_manager.create_all_datasets() if online_train_dataset is None: @@ -216,7 +237,7 @@ def collate_fn(batch): # -------------------------------------------------------------------------- # 6. Main loop: forward target model, save hidden states # -------------------------------------------------------------------------- - global_idx = 0 # index within this rank's portion + global_idx = 0 # index within this rank's portion total = len(dataloader) t0 = time.time() @@ -224,7 +245,7 @@ def collate_fn(batch): input_ids = batch["input_ids"] # Shape may be [B, 1, S] or [B, S] depending on how dataset stores it if input_ids.dim() == 3: - input_ids = input_ids.squeeze(1) # → [B, S] + input_ids = input_ids.squeeze(1) # → [B, S] attention_mask = batch.get("attention_mask", torch.ones_like(input_ids)) if attention_mask.dim() == 3: @@ -261,10 +282,10 @@ def collate_fn(batch): ckpt_path = save_dir / f"sample_{sample_idx:08d}_rank{rank}.ckpt" ckpt = { - "input_ids": input_ids[i : i + 1].cpu(), # [1, S] - "hidden_states": hidden_states[i : i + 1].cpu().to(torch.bfloat16), # [1, S, D*L] - "loss_mask": loss_mask[i : i + 1].cpu(), # [1, S] - "attention_mask": attention_mask[i : i + 1].cpu(), # [1, S] + "input_ids": input_ids[i : i + 1].cpu(), # [1, S] + "hidden_states": hidden_states[i : i + 1].cpu().to(torch.bfloat16), # [1, S, D*L] + "loss_mask": loss_mask[i : i + 1].cpu(), # [1, S] + "attention_mask": attention_mask[i : i + 1].cpu(), # [1, S] } torch.save(ckpt, ckpt_path) @@ -281,10 +302,7 @@ def collate_fn(batch): if world_size > 1: dist.barrier() - rank0_print( - f"Data generation complete. " - f"Saved files to {output_dir}" - ) + rank0_print(f"Data generation complete. " f"Saved files to {output_dir}") if __name__ == "__main__": diff --git a/tools/train_dflash_offline.py b/tools/train_dflash_offline.py index a9231b89..1ac03d33 100755 --- a/tools/train_dflash_offline.py +++ b/tools/train_dflash_offline.py @@ -16,7 +16,6 @@ import torch import transformers -from transformers import AutoTokenizer from angelslim.compressor.speculative import ( DraftModelConfig, @@ -24,24 +23,19 @@ create_draft_model, get_supported_chat_template_type_strings, ) - -from angelslim.compressor.speculative.train.models.draft.online_dflash_model import ( - OnlineDFlashModel, -) -from angelslim.compressor.speculative.train.trainer.online_dflash_trainer import ( - OnlineDFlashTrainer, +from angelslim.compressor.speculative.train.data.data_utils import ( + DataCollatorWithPadding, ) from angelslim.compressor.speculative.train.data.dataset_builder.offline_dataset_builder import ( OfflineEagle3Dataset, ) -from angelslim.compressor.speculative.train.data.data_utils import DataCollatorWithPadding from angelslim.utils import rank0_print - # --------------------------------------------------------------------------- # Offline DFlash Dataset # --------------------------------------------------------------------------- + class OfflineDFlashDataset(OfflineEagle3Dataset): """ DFlash variant of the offline dataset. @@ -61,6 +55,7 @@ class OfflineDFlashDataset(OfflineEagle3Dataset): def _load_ckpt(self, idx: int): import warnings + ckpt_path = self.ckpt_files[idx] try: data = torch.load(ckpt_path, map_location="cpu", weights_only=False) @@ -70,9 +65,7 @@ def _load_ckpt(self, idx: int): missing = [k for k in self.REQUIRED_KEYS if k not in data] if missing: - warnings.warn( - f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning - ) + warnings.warn(f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning) return None # Auto-generate attention_mask if absent @@ -86,6 +79,7 @@ def _load_ckpt(self, idx: int): # Argument parser # --------------------------------------------------------------------------- + def parse_args(): parser = argparse.ArgumentParser(description="Train DFlash draft model (offline mode)") @@ -93,8 +87,9 @@ def parse_args(): m = parser.add_argument_group("Model Arguments") m.add_argument("--target_model_name_or_path", type=str, required=True) m.add_argument("--draft_model_config_path", type=str, required=True) - m.add_argument("--torch_dtype", type=str, default="bfloat16", - choices=["float16", "bfloat16", "float32"]) + m.add_argument( + "--torch_dtype", type=str, default="bfloat16", choices=["float16", "bfloat16", "float32"] + ) m.add_argument("--trust_remote_code", action="store_true", default=True) m.add_argument("--embed_weight_key", type=str, default="model.embed_tokens.weight") m.add_argument("--lm_head_key", type=str, default="lm_head.weight") @@ -105,21 +100,38 @@ def parse_args(): d.add_argument("--num_anchors", type=int, default=None) d.add_argument("--loss_decay_gamma", type=float, default=None) d.add_argument("--mask_token_id", type=int, default=None) - d.add_argument("--attention_backend", type=str, default=None, - choices=["flex_attention", "sdpa", "eager"]) + d.add_argument( + "--attention_backend", type=str, default=None, choices=["flex_attention", "sdpa", "eager"] + ) # Data da = parser.add_argument_group("Data Arguments") - da.add_argument("--train_hidden_path", type=str, required=True, - help="Directory of pre-computed training .ckpt files") - da.add_argument("--eval_hidden_path", type=str, default=None, - help="Directory of pre-computed eval .ckpt files (optional)") - da.add_argument("--chat_template_type", type=str, default="qwen3", - help=f"Supported: {', '.join(get_supported_chat_template_type_strings())}") + da.add_argument( + "--train_hidden_path", + type=str, + required=True, + help="Directory of pre-computed training .ckpt files", + ) + da.add_argument( + "--eval_hidden_path", + type=str, + default=None, + help="Directory of pre-computed eval .ckpt files (optional)", + ) + da.add_argument( + "--chat_template_type", + type=str, + default="qwen3", + help=f"Supported: {', '.join(get_supported_chat_template_type_strings())}", + ) da.add_argument("--model_max_length", type=int, default=3072) da.add_argument("--num_proc", type=int, default=16) - da.add_argument("--cache_in_memory", action="store_true", default=False, - help="Cache all .ckpt files in RAM (fast but memory-intensive)") + da.add_argument( + "--cache_in_memory", + action="store_true", + default=False, + help="Cache all .ckpt files in RAM (fast but memory-intensive)", + ) # Training t = parser.add_argument_group("Training Arguments") @@ -169,6 +181,7 @@ def _setup_wandb(args): if local_rank == 0: try: import wandb + wandb.init( project=os.environ.get("WANDB_PROJECT", "angelslim-dflash"), name=run_name, @@ -182,12 +195,13 @@ def _setup_wandb(args): # Main # --------------------------------------------------------------------------- + def train(): args = parse_args() _setup_wandb(args) dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} - torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) + #torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) # ------------------------------------------------------------------ # 1. Draft model config @@ -218,9 +232,7 @@ def train(): # ------------------------------------------------------------------ rank0_print("Loading draft model...") draft_model = create_draft_model(draft_model_config) - rank0_print( - f"Draft model parameters: {sum(p.numel() for p in draft_model.parameters()):,}" - ) + rank0_print(f"Draft model parameters: {sum(p.numel() for p in draft_model.parameters()):,}") # ------------------------------------------------------------------ # 3. Offline datasets @@ -283,7 +295,7 @@ def train(): training_mode="offline", modal_type="DFlash", draft_model=draft_model, - target_model=None, # Not needed — hidden states are pre-computed + target_model=None, # Not needed — hidden states are pre-computed length=args.training_time_test_length, draft_model_config=draft_model_config, args=training_args, diff --git a/tools/train_dflash_online.py b/tools/train_dflash_online.py index 7c6171bf..5a33a16d 100755 --- a/tools/train_dflash_online.py +++ b/tools/train_dflash_online.py @@ -278,9 +278,7 @@ def parse_args(): default="cosine", help="Learning rate scheduler type", ) - training_group.add_argument( - "--run_name", type=str, default=None, help="Run name for tracking" - ) + training_group.add_argument("--run_name", type=str, default=None, help="Run name for tracking") training_group.add_argument( "--report_to", type=str, @@ -320,11 +318,7 @@ def _setup_wandb(args) -> None: os.environ["WANDB_PROJECT"] = args.wandb_project # Resolve run name: --wandb_run_name > --run_name > env WANDB_RUN_NAME - run_name = ( - args.wandb_run_name - or args.run_name - or os.environ.get("WANDB_RUN_NAME") - ) + run_name = args.wandb_run_name or args.run_name or os.environ.get("WANDB_RUN_NAME") if run_name: os.environ["WANDB_RUN_NAME"] = run_name # Propagate back so TrainingArguments picks it up @@ -342,10 +336,7 @@ def _setup_wandb(args) -> None: resume="allow", ) except ImportError: - print( - "[WARNING] wandb not installed. " - "Install via: pip install wandb" - ) + print("[WARNING] wandb not installed. " "Install via: pip install wandb") def train(): @@ -380,7 +371,10 @@ def train(): if args.attention_backend is not None: draft_model_config.attention_backend = args.attention_backend if args.mask_token_id is not None: - if not hasattr(draft_model_config, "dflash_config") or draft_model_config.dflash_config is None: + if ( + not hasattr(draft_model_config, "dflash_config") + or draft_model_config.dflash_config is None + ): draft_model_config.dflash_config = {} draft_model_config.dflash_config["mask_token_id"] = args.mask_token_id @@ -412,9 +406,7 @@ def train(): rank0_print(f"draft_model_config: {draft_model_config}") draft_model = create_draft_model(draft_model_config) rank0_print("Draft model loaded successfully") - rank0_print( - f"Draft model parameters: {sum(p.numel() for p in draft_model.parameters()):,}" - ) + rank0_print(f"Draft model parameters: {sum(p.numel() for p in draft_model.parameters()):,}") # Create datasets using DatasetManager rank0_print( From f56544fe4e0afbb489d42115b7ccf08dd0e22b99 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 16 Mar 2026 20:51:09 +0800 Subject: [PATCH 12/23] dev_dflash --- tools/train_dflash_offline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/train_dflash_offline.py b/tools/train_dflash_offline.py index 1ac03d33..22632ceb 100755 --- a/tools/train_dflash_offline.py +++ b/tools/train_dflash_offline.py @@ -60,12 +60,12 @@ def _load_ckpt(self, idx: int): try: data = torch.load(ckpt_path, map_location="cpu", weights_only=False) except Exception as e: - warnings.warn(f"Failed to load {ckpt_path}: {e}. Skipping.", RuntimeWarning) + warnings.warn(f"Failed to load {ckpt_path}: {e}. Skipping.", RuntimeWarning, stacklevel=2) return None missing = [k for k in self.REQUIRED_KEYS if k not in data] if missing: - warnings.warn(f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning) + warnings.warn(f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning, stacklevel=2) return None # Auto-generate attention_mask if absent @@ -200,8 +200,8 @@ def train(): args = parse_args() _setup_wandb(args) - dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} - #torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) + #dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} + # torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) # ------------------------------------------------------------------ # 1. Draft model config From 764c83aa6ae8880bacefd2a327a36dfc604495c8 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 16 Mar 2026 20:57:26 +0800 Subject: [PATCH 13/23] dev_dflash --- tools/train_dflash_offline.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/train_dflash_offline.py b/tools/train_dflash_offline.py index 22632ceb..4f8ad528 100755 --- a/tools/train_dflash_offline.py +++ b/tools/train_dflash_offline.py @@ -60,12 +60,16 @@ def _load_ckpt(self, idx: int): try: data = torch.load(ckpt_path, map_location="cpu", weights_only=False) except Exception as e: - warnings.warn(f"Failed to load {ckpt_path}: {e}. Skipping.", RuntimeWarning, stacklevel=2) + warnings.warn( + f"Failed to load {ckpt_path}: {e}. Skipping.", RuntimeWarning, stacklevel=2 + ) return None missing = [k for k in self.REQUIRED_KEYS if k not in data] if missing: - warnings.warn(f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning, stacklevel=2) + warnings.warn( + f"{ckpt_path} missing keys {missing}. Skipping.", RuntimeWarning, stacklevel=2 + ) return None # Auto-generate attention_mask if absent @@ -200,7 +204,7 @@ def train(): args = parse_args() _setup_wandb(args) - #dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} + # dtype_map = {"float16": torch.float16, "bfloat16": torch.bfloat16, "float32": torch.float32} # torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16) # ------------------------------------------------------------------ From 3e3d4b8df341c4549a8711fae48153b81c2e3c52 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Sat, 30 May 2026 16:42:47 +0800 Subject: [PATCH 14/23] dflare --- .../speculative/train/data/data_utils.py | 1 - .../train/models/draft/__init__.py | 2 + .../train/models/draft/qwen_dflare.py | 466 ++++++++++++++++++ .../train/models/draft/qwen_dflash.py | 6 +- .../models/target/target_model_wrapper.py | 92 +++- .../train/trainer/online_dflash_trainer.py | 441 ++++++++++++++++- configs/fsdp_config.json | 6 + configs/qwen3_dflare.json | 60 +++ scripts/speculative/run_dflare_online.sh | 55 +++ scripts/speculative/run_dflash_aligned.sh | 143 ++++++ scripts/speculative/run_dflash_online.sh | 6 +- tools/train_dflash_online.py | 81 ++- 12 files changed, 1333 insertions(+), 26 deletions(-) create mode 100644 angelslim/compressor/speculative/train/models/draft/qwen_dflare.py create mode 100644 configs/fsdp_config.json create mode 100644 configs/qwen3_dflare.json create mode 100755 scripts/speculative/run_dflare_online.sh create mode 100755 scripts/speculative/run_dflash_aligned.sh diff --git a/angelslim/compressor/speculative/train/data/data_utils.py b/angelslim/compressor/speculative/train/data/data_utils.py index 7725af78..bfeab8ba 100644 --- a/angelslim/compressor/speculative/train/data/data_utils.py +++ b/angelslim/compressor/speculative/train/data/data_utils.py @@ -52,7 +52,6 @@ def convert_ultrachat_data(row, dataset_column="messages"): return {"conversations": converted_messages, "id": row["prompt_id"]} -# Copied from https://github.com/sgl-project/SpecForge/blob/main/specforge/data/preprocessing.py # noqa: E501 def process_token_dict_to_mappings( token_dict, draft_vocab_size: int, diff --git a/angelslim/compressor/speculative/train/models/draft/__init__.py b/angelslim/compressor/speculative/train/models/draft/__init__.py index c056ce23..58940d9f 100644 --- a/angelslim/compressor/speculative/train/models/draft/__init__.py +++ b/angelslim/compressor/speculative/train/models/draft/__init__.py @@ -14,6 +14,7 @@ from .draft_model_factory import DraftModelConfig, create_draft_model from .llama_eagle3 import CosyVoice3Eagle3LlamaForCausalLM, Eagle3LlamaForCausalLM +from .qwen_dflare import QwenDFlareDraftModel from .qwen_dflash import QwenDFlashDraftModel __all__ = [ @@ -22,4 +23,5 @@ "Eagle3LlamaForCausalLM", "CosyVoice3Eagle3LlamaForCausalLM", "QwenDFlashDraftModel", + "QwenDFlareDraftModel", ] diff --git a/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py b/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py new file mode 100644 index 00000000..5905b43f --- /dev/null +++ b/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py @@ -0,0 +1,466 @@ +# Copyright 2025 Tencent Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DFlare Draft Model for Qwen3 architecture. + +AngelSlim DFlare is an enhanced DFlash variant with two structural changes +compared to ``QwenDFlashDraftModel``: + +1. Cross-attention uses **separate** k/v projections for context (target hidden + states) vs. noise (draft tokens): ``k_proj_target/v_proj_target`` for + context, ``k_proj/v_proj`` for noise. +2. Multi-layer target hidden states are fused via a learnable + ``layer_fusion_weights[D, T]`` matrix (softmax-normalised, einsum'd) instead + of a single ``Linear(T*H -> H)`` projection. Each draft layer learns its own + weighted combination of the T captured target layers. + +Training-side logic (anchor sampling, BlockMask, weighted CE loss, accuracy) +is **identical** to DFlash, so this model is consumed by the same +``OnlineDFlashTrainer``. +""" + +from typing import Callable, List, Optional, Tuple + +import torch +from torch import nn +from transformers import DynamicCache +from transformers.cache_utils import Cache +from transformers.models.qwen3.modeling_qwen3 import ( + ALL_ATTENTION_FUNCTIONS, + FlashAttentionKwargs, + GradientCheckpointingLayer, + Qwen3Config, + Qwen3MLP, + Qwen3PreTrainedModel, + Qwen3RMSNorm, + Qwen3RotaryEmbedding, + eager_attention_forward, + rotate_half, +) +from typing_extensions import Unpack + +from .draft_model_factory import DraftModelFactory + + +def sample(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor: + if temperature < 1e-5: + return torch.argmax(logits, dim=-1) + bsz, seq_len, vocab_size = logits.shape + logits = logits.view(-1, vocab_size) + logits = logits / temperature + probs = torch.softmax(logits, dim=-1) + return torch.multinomial(probs, num_samples=1).view(bsz, seq_len) + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_len = q.size(-2) + q_embed = (q * cos[..., -q_len:, :]) + (rotate_half(q) * sin[..., -q_len:, :]) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + +def build_target_layer_ids( + num_target_layers: int, num_draft_layers: int +) -> List[int]: + """Compute target layer IDs to capture from the target model.""" + if num_draft_layers == 1: + return [(num_target_layers // 2)] + start = 1 + end = num_target_layers - 3 + span = end - start + target_layer_ids = [ + int(round(start + (i * span) / (num_draft_layers - 1))) + for i in range(num_draft_layers) + ] + return target_layer_ids + + +def extract_context_feature( + hidden_states: list, + layer_ids: Optional[List[int]], +) -> torch.Tensor: + """Extract and concatenate hidden states from specified layers.""" + offset = 1 + selected_states = [] + for layer_id in layer_ids: + selected_states.append(hidden_states[layer_id + offset]) + target_hidden = torch.cat(selected_states, dim=-1) + return target_hidden + + +class Qwen3DFlareAttention(nn.Module): + """Multi-headed cross-attention for DFlare. + + Q comes from draft hidden states. KV is the concatenation of context + (target hidden) and noise (draft) projections, but unlike DFlash the + context uses dedicated ``k_proj_target / v_proj_target`` parameters. + """ + + def __init__(self, config: Qwen3Config, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr( + config, "head_dim", config.hidden_size // config.num_attention_heads + ) + self.num_key_value_groups = ( + config.num_attention_heads // config.num_key_value_heads + ) + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = False + self.q_proj = nn.Linear( + config.hidden_size, + config.num_attention_heads * self.head_dim, + bias=config.attention_bias, + ) + self.k_proj = nn.Linear( + config.hidden_size, + config.num_key_value_heads * self.head_dim, + bias=config.attention_bias, + ) + self.k_proj_target = nn.Linear( + config.hidden_size, + config.num_key_value_heads * self.head_dim, + bias=config.attention_bias, + ) + self.v_proj = nn.Linear( + config.hidden_size, + config.num_key_value_heads * self.head_dim, + bias=config.attention_bias, + ) + self.v_proj_target = nn.Linear( + config.hidden_size, + config.num_key_value_heads * self.head_dim, + bias=config.attention_bias, + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, + config.hidden_size, + bias=config.attention_bias, + ) + self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) + self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) + self.sliding_window = ( + config.sliding_window + if config.layer_types[layer_idx] == "sliding_attention" + else None + ) + + def forward( + self, + hidden_states: torch.Tensor, + target_hidden: torch.Tensor, + position_embeddings: Tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + bsz, q_len = hidden_states.shape[:-1] + ctx_len = target_hidden.shape[1] + q = self.q_proj(hidden_states) + q = q.view(bsz, q_len, -1, self.head_dim) + q = self.q_norm(q).transpose(1, 2) + k_ctx = self.k_proj_target(target_hidden) + k_noise = self.k_proj(hidden_states) + v_ctx = self.v_proj_target(target_hidden) + v_noise = self.v_proj(hidden_states) + k = torch.cat([k_ctx, k_noise], dim=1).view( + bsz, ctx_len + q_len, -1, self.head_dim + ) + v = torch.cat([v_ctx, v_noise], dim=1).view( + bsz, ctx_len + q_len, -1, self.head_dim + ) + k = self.k_norm(k).transpose(1, 2) + v = v.transpose(1, 2) + cos, sin = position_embeddings + q, k = apply_rotary_pos_emb(q, k, cos, sin) + if past_key_values is not None: + cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} + k, v = past_key_values.update(k, v, self.layer_idx, cache_kwargs) + attn_fn: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + attn_output, attn_weights = attn_fn( + self, + q, + k, + v, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, + ) + attn_output = attn_output.reshape(bsz, q_len, -1) + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + +class Qwen3DFlareDecoderLayer(GradientCheckpointingLayer): + """DFlare decoder layer with cross-attention to context.""" + + def __init__(self, config: Qwen3Config, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + self.self_attn = Qwen3DFlareAttention(config=config, layer_idx=layer_idx) + self.mlp = Qwen3MLP(config) + self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen3RMSNorm( + config.hidden_size, eps=config.rms_norm_eps + ) + + def forward( + self, + target_hidden: Optional[torch.Tensor] = None, + hidden_states: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_value: Optional[Cache] = None, + output_attentions: Optional[bool] = False, + use_cache: Optional[bool] = False, + cache_position: Optional[torch.LongTensor] = None, + position_embeddings: Optional[ + Tuple[torch.Tensor, torch.Tensor] + ] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> torch.FloatTensor: + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + hidden_states = self.self_attn( + hidden_states=hidden_states, + target_hidden=target_hidden, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_value, + output_attentions=output_attentions, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + )[0] + hidden_states = residual + hidden_states + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + return hidden_states + + +@DraftModelFactory.register +class QwenDFlareDraftModel(Qwen3PreTrainedModel): + """DFlare Draft Model for Qwen3 architecture. + + Same input/output contract as ``QwenDFlashDraftModel`` (consumed by the + same ``OnlineDFlashTrainer``), with two structural improvements: + * separate context/noise k,v projections inside cross-attention; + * learnable per-draft-layer fusion weights over target layers. + """ + + config_class = Qwen3Config + _no_split_modules = ["Qwen3DFlareDecoderLayer"] + + def __init__(self, config) -> None: + super().__init__(config) + self.config = config + self.layers = nn.ModuleList( + [ + Qwen3DFlareDecoderLayer(config, layer_idx) + for layer_idx in range(config.num_hidden_layers) + ] + ) + self.num_draft_layers = config.num_hidden_layers + # We intentionally read from ``dflash_config`` to remain compatible with + # the existing trainer, which extracts ``mask_token_id`` etc. from the + # same key. + dflash_config = getattr(config, "dflash_config", {}) or {} + self.target_layer_ids = dflash_config.get( + "target_layer_ids", + build_target_layer_ids(config.num_target_layers, config.num_hidden_layers), + ) + self.num_target_layers = len(self.target_layer_ids) + self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3RotaryEmbedding(config) + self.layer_fusion_weights = nn.Parameter( + torch.empty(self.num_draft_layers, self.num_target_layers) + ) + self._init_fusion_weights() + self.hidden_norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.block_size = config.block_size + self.mask_token_id = dflash_config.get("mask_token_id", None) + self.post_init() + + def _init_fusion_weights(self) -> None: + nn.init.constant_(self.layer_fusion_weights, 0.0) + for d_idx in range(self.num_draft_layers): + t_idx = min( + self.num_target_layers - 1, + int((d_idx / self.num_draft_layers) * self.num_target_layers), + ) + self.layer_fusion_weights.data[d_idx, t_idx] = 2.0 + + def forward( + self, + position_ids: torch.LongTensor, + attention_mask: Optional[torch.Tensor] = None, + noise_embedding: Optional[torch.Tensor] = None, + target_hidden: Optional[torch.Tensor] = None, + past_key_values: Optional[Cache] = None, + use_cache: bool = False, + **kwargs, + ) -> torch.Tensor: + hidden_states = noise_embedding + bsz, seq_len, _ = target_hidden.shape + # target_hidden arrives as concatenation along feature dim of T target + # layers' hidden states: [B, S, T*H]. Reshape to per-layer tensor. + target_hidden_reshaped = target_hidden.view( + bsz, seq_len, self.num_target_layers, self.config.hidden_size + ) + fusion_probs = torch.softmax(self.layer_fusion_weights, dim=1) + # bsth (target) x dt (per-draft-layer fusion) -> bsdh (per-draft-layer) + fused_hidden = torch.einsum( + "bsth,dt->bsdh", target_hidden_reshaped, fusion_probs + ) + fused_hidden = self.hidden_norm(fused_hidden) + position_embeddings = self.rotary_emb(hidden_states, position_ids) + for i, layer in enumerate(self.layers): + layer_target_hidden = fused_hidden[:, :, i, :] + hidden_states = layer( + hidden_states=hidden_states, + target_hidden=layer_target_hidden, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_value=past_key_values, + use_cache=use_cache, + position_embeddings=position_embeddings, + **kwargs, + ) + return self.norm(hidden_states) + + @torch.inference_mode() + def spec_generate( + self, + target: nn.Module, + input_ids: torch.LongTensor, + max_new_tokens: int, + stop_token_ids: List[int], + temperature: float, + ): + """Speculative generation with DFlare draft model.""" + self.eval() + num_input_tokens = input_ids.shape[1] + max_length = num_input_tokens + max_new_tokens + + block_size = self.block_size + output_ids = torch.full( + (1, max_length + block_size), + self.mask_token_id, + dtype=torch.long, + device=target.device, + ) + position_ids = torch.arange( + output_ids.shape[1], device=target.device + ).unsqueeze(0) + + past_key_values_target = DynamicCache() + past_key_values_draft = DynamicCache() + + # Prefill stage + output = target( + input_ids, + position_ids=position_ids[:, :num_input_tokens], + past_key_values=past_key_values_target, + use_cache=True, + logits_to_keep=1, + output_hidden_states=True, + ) + + output_ids[:, :num_input_tokens] = input_ids + output_ids[:, num_input_tokens : num_input_tokens + 1] = sample( + output.logits, temperature + ) + target_hidden = extract_context_feature( + output.hidden_states, self.target_layer_ids + ) + + # Decode stage + acceptance_lengths = [] + start = input_ids.shape[1] + while start < max_length: + block_output_ids = output_ids[:, start : start + block_size].clone() + block_position_ids = position_ids[:, start : start + block_size] + noise_embedding = target.model.embed_tokens(block_output_ids) + draft_logits = target.lm_head( + self( + target_hidden=target_hidden, + noise_embedding=noise_embedding, + position_ids=position_ids[ + :, past_key_values_draft.get_seq_length() : start + block_size + ], + past_key_values=past_key_values_draft, + use_cache=True, + is_causal=False, + )[:, -block_size + 1 :, :] + ) + past_key_values_draft.crop(start) + block_output_ids[:, 1:] = sample(draft_logits) + + output = target( + block_output_ids, + position_ids=block_position_ids, + past_key_values=past_key_values_target, + use_cache=True, + output_hidden_states=True, + ) + + posterior = sample(output.logits, temperature) + acceptance_length = ( + (block_output_ids[:, 1:] == posterior[:, :-1]) + .cumprod(dim=1) + .sum(dim=1)[0] + .item() + ) + output_ids[:, start : start + acceptance_length + 1] = block_output_ids[ + :, : acceptance_length + 1 + ] + output_ids[:, start + acceptance_length + 1] = posterior[ + :, acceptance_length + ] + start += acceptance_length + 1 + past_key_values_target.crop(start) + target_hidden = extract_context_feature( + output.hidden_states, self.target_layer_ids + )[:, : acceptance_length + 1, :] + acceptance_lengths.append(acceptance_length + 1) + if stop_token_ids is not None and any( + stop_token_id in output_ids[:, num_input_tokens:] + for stop_token_id in stop_token_ids + ): + break + output_ids = output_ids[:, :max_length] + output_ids = output_ids[:, output_ids[0] != self.mask_token_id] + if stop_token_ids is not None: + stop_token_ids = torch.tensor(stop_token_ids, device=output_ids.device) + stop_token_indices = torch.isin( + output_ids[0][num_input_tokens:], stop_token_ids + ).nonzero(as_tuple=True)[0] + if stop_token_indices.numel() > 0: + output_ids = output_ids[ + :, : num_input_tokens + stop_token_indices[0] + 1 + ] + + return output_ids diff --git a/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py b/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py index faf00343..ef824c0b 100755 --- a/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py +++ b/angelslim/compressor/speculative/train/models/draft/qwen_dflash.py @@ -14,9 +14,9 @@ """DFlash Draft Model for Qwen3 architecture. -Migrated from SpecForge's specforge/modeling/draft/dflash.py. -Uses cross-attention between draft blocks and context hidden states, -fundamentally different from Eagle3's concat + self-attention approach. +AngelSlim DFlash draft model using cross-attention between draft blocks and +context hidden states from the target model — fundamentally different from +Eagle3's concat + self-attention approach. """ from typing import Callable, List, Optional, Tuple diff --git a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py index e803ee27..b8438be9 100644 --- a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py +++ b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py @@ -173,12 +173,22 @@ def load_model(self) -> None: # Prepare model loading configuration model_kwargs = self._prepare_model_kwargs(device) + print_with_rank( + f"Target model attn_implementation: " + f"{model_kwargs.get('attn_implementation', 'NOT SET (will use HF default)')}" + ) # Load and configure model self.model = AutoModelForCausalLM.from_pretrained(self.model_path, **model_kwargs) self._freeze_model_parameters() self.model.eval() + # Verify attention implementation actually used + _actual_attn = getattr(self.model.config, "_attn_implementation", "unknown") + print_with_rank( + f"Target model loaded. Actual attn_implementation: {_actual_attn}" + ) + # Load tokenizer self.tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True) @@ -196,8 +206,15 @@ def _prepare_model_kwargs(self, device: str) -> dict: "torch_dtype": torch.bfloat16, "device_map": device, "trust_remote_code": True, + # AngelSlim DFlash uses flash_attention_2 by default to match + # the inference kernel and avoid train/test mismatch. + "attn_implementation": "flash_attention_2", } - default_kwargs.update(self.kwargs) + # Only pass through kwargs that are valid for from_pretrained; + # filter out non-model kwargs like modal_type, target_model_type, etc. + _non_model_keys = {"modal_type", "target_model_type"} + filtered = {k: v for k, v in self.kwargs.items() if k not in _non_model_keys} + default_kwargs.update(filtered) return default_kwargs def _freeze_model_parameters(self) -> None: @@ -214,29 +231,72 @@ def get_hidden_states_and_logits( """ Extract hidden states and logits using Transformers backend. + Processes each sample INDIVIDUALLY (without padding). Batch processing + with padding causes numerical differences in hidden states even with + flash_attention_2, because padding tokens still participate in + attention and leak into other positions' representations. + Args: - input_ids: Input token IDs - attention_mask: Attention mask + input_ids: Input token IDs [batch_size, seq_len] + attention_mask: Attention mask [batch_size, seq_len] **kwargs: May contain 'aux_hidden_states_layer_ids' to specify custom layers Returns: - Tuple of (concatenated_hidden_states, logits) + Tuple of (concatenated_hidden_states, logits) padded back to + [batch_size, seq_len, ...] so downstream collator/trainer code is + unchanged. Padding positions in the returned tensors are zero; + they are masked out by ``loss_mask`` later. """ - with torch.no_grad(): - outputs = self.model( - input_ids, - attention_mask=attention_mask, - output_hidden_states=True, - output_logits=True, - use_cache=False, # match SpecForge: no KV-cache during training + aux_layer_ids = kwargs.get("aux_hidden_states_layer_ids", None) + bsz, seq_len = input_ids.shape + + # Process each sample individually to avoid padding-induced hidden + # state corruption (AngelSlim DFlash per-sample processing). + hidden_list = [] + logits_list = [] + + for i in range(bsz): + # Determine actual (non-padded) length for this sample + if attention_mask is not None: + actual_len = int(attention_mask[i].sum().item()) + else: + actual_len = seq_len + + # Extract the unpadded portion + single_ids = input_ids[i:i + 1, :actual_len] + + with torch.no_grad(): + outputs = self.model( + single_ids, + output_hidden_states=True, + use_cache=False, # AngelSlim DFlash: no KV-cache during training + ) + + # Extract auxiliary hidden states for this sample + # h shape: [1, actual_len, D*num_layers] + h = self._extract_auxiliary_hidden_states( + outputs.hidden_states, aux_layer_ids ) - # Extract auxiliary hidden states - aux_layer_ids = kwargs.get("aux_hidden_states_layer_ids", None) - hidden_states = self._extract_auxiliary_hidden_states(outputs.hidden_states, aux_layer_ids) + # Pad back to seq_len to maintain batch shape + if actual_len < seq_len: + pad_size = seq_len - actual_len + # Pad seq dim only (last-but-one dim); hidden dim is untouched + h = torch.nn.functional.pad(h, (0, 0, 0, pad_size)) + logits_padded = torch.nn.functional.pad( + outputs.logits, (0, 0, 0, pad_size) + ) + else: + logits_padded = outputs.logits - # Return hidden states and logits on the same device as input - return hidden_states, outputs.logits.to(input_ids.device) + hidden_list.append(h) + logits_list.append(logits_padded) + + # Stack back to batch + hidden_states = torch.cat(hidden_list, dim=0) # [B, seq_len, D*num_layers] + logits = torch.cat(logits_list, dim=0) # [B, seq_len, vocab] + + return hidden_states, logits.to(input_ids.device) def get_aux_and_target_hiddens( self, diff --git a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py index e2ff3741..90d8712e 100755 --- a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py +++ b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py @@ -206,6 +206,257 @@ def _load_file_content( self.lm_head.weight.data.copy_(tensor) +class _FP32StateAdamW(torch.optim.Optimizer): + """AdamW with fp32 master weights (AngelSlim DFlash optimizer). + + Maintains fp32 master copies of all parameters (in optimizer state). + On each step: + 1. Cast bf16 gradients to fp32. + 2. Clip fp32 grad norm. + 3. Adam update on fp32 master weights. + 4. Copy fp32 master -> bf16 model params. + + Key properties: + * Accumulation in fp32 (no precision loss from bf16 quantization). + * Grad clipping on fp32 gradients. + * Only the final copy-back introduces bf16 quantization. + + Compatible with FSDP + accelerate + HF Trainer (operates on the SAME + parameter objects required for FSDP state_dict). + """ + + def __init__( + self, + params, + lr=1e-3, + betas=(0.9, 0.999), + eps=1e-8, + weight_decay=0.0, + max_grad_norm=1.0, + ): + defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay) + self.max_grad_norm = max_grad_norm + super().__init__(params, defaults) + + # Eagerly initialize all master parameters at construction so all + # ranks start from synchronized bf16 params before any training step. + with torch.no_grad(): + for group in self.param_groups: + for p in group["params"]: + state = self.state[p] + state["step"] = torch.tensor(0.0) + state["exp_avg"] = torch.zeros_like(p, dtype=torch.float32) + state["exp_avg_sq"] = torch.zeros_like(p, dtype=torch.float32) + state["master_param"] = p.data.detach().clone().to(torch.float32) + + @torch.no_grad() + def step(self, closure=None): + """Full fp32 master-weight update step (AngelSlim DFlash optimizer).""" + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + # Phase 1: Cast all bf16 grads to fp32 (kept temporarily in state). + all_fp32_grads = [] + for group in self.param_groups: + for p in group["params"]: + if p.grad is None: + continue + + state = self.state[p] + + # Ensure states are fp32 (handles resume from checkpoint). + if state["exp_avg"].dtype != torch.float32: + state["exp_avg"] = state["exp_avg"].to(torch.float32) + if state["exp_avg_sq"].dtype != torch.float32: + state["exp_avg_sq"] = state["exp_avg_sq"].to(torch.float32) + if state["master_param"].dtype != torch.float32: + state["master_param"] = state["master_param"].to(torch.float32) + + fp32_grad = p.grad.detach().to(torch.float32) + state["_fp32_grad"] = fp32_grad + all_fp32_grads.append(fp32_grad) + + # Phase 2: Clip fp32 grad norm. + # Manual clipping because all_fp32_grads holds plain tensors (not Parameters). + # In FSDP SHARD_GRAD_OP + use_orig_params=True, p.grad is the full + # all-reduced gradient (same on all ranks), so per-rank clipping is correct. + if self.max_grad_norm > 0 and all_fp32_grads: + total_norm_sq = sum(g.norm().pow(2) for g in all_fp32_grads) + total_norm = total_norm_sq.sqrt() + clip_coef = self.max_grad_norm / (total_norm + 1e-6) + clip_coef_clamped = min(clip_coef.item(), 1.0) + if clip_coef_clamped < 1.0: + for g in all_fp32_grads: + g.mul_(clip_coef_clamped) + + # Phase 3: Adam update on fp32 master weights, then copy back to bf16. + for group in self.param_groups: + lr = group["lr"] + beta1, beta2 = group["betas"] + eps = group["eps"] + weight_decay = group["weight_decay"] + + for p in group["params"]: + if p.grad is None: + continue + + state = self.state[p] + grad = state.pop("_fp32_grad") + + exp_avg = state["exp_avg"] + exp_avg_sq = state["exp_avg_sq"] + master_param = state["master_param"] + + state["step"] += 1 + step_t = state["step"].item() + + # Decoupled weight decay on fp32 master (AdamW style). + if weight_decay != 0: + master_param.mul_(1.0 - lr * weight_decay) + + # Adam update in fp32. + exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) + exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) + + bias_correction1 = 1 - beta1 ** step_t + bias_correction2 = 1 - beta2 ** step_t + + step_size = lr / bias_correction1 + denom = (exp_avg_sq.sqrt() / (bias_correction2 ** 0.5)).add_(eps) + + master_param.addcdiv_(exp_avg, denom, value=-step_size) + + # Copy fp32 master -> bf16 model param (only quantization point). + p.data.copy_(master_param.to(p.dtype)) + p.grad = None + + return loss + + +class _FP32MasterWeightOptimizer(torch.optim.Optimizer): + """Thin wrapper around any torch optimizer that maintains fp32 master weights. + + AngelSlim DFlash fp32 master-weight pattern: + 1. At construction, clone bf16 model params -> fp32 master copies. + 2. On every step(): + a. Copy bf16 grads -> fp32 master grads (cast up), clear bf16 grads. + b. Clip fp32 grad norm. + c. Run inner optimizer step on fp32 params. + d. Copy updated fp32 values -> bf16 model params (cast down). + + Used as ``self.optimizer`` inside HF Trainer for the DDP code path. + HF Trainer calls ``self.optimizer.step()`` / ``self.optimizer.zero_grad()`` + directly, so placing the sync logic inside the optimizer is the only + reliable way to ensure it actually runs. + + Inherits from torch.optim.Optimizer so isinstance checks in HF Trainer's + lr_scheduler creation (LambdaLR.__init__) pass correctly. + """ + + def __init__( + self, + bf16_params: List[torch.Tensor], + inner_optimizer: torch.optim.Optimizer, + max_grad_norm: float = 1.0, + ): + self._bf16_params = bf16_params + + # Build fp32 master copies and replace the optimizer's param groups. + self._fp32_params: List[torch.Tensor] = [ + p.detach().clone().to(torch.float32).requires_grad_(True) + for p in bf16_params + ] + assert len(inner_optimizer.param_groups) == 1, ( + "_FP32MasterWeightOptimizer expects a single param group; " + "extend if/when multiple groups (e.g. LoRA, lr split) are needed." + ) + inner_optimizer.param_groups[0]["params"] = self._fp32_params + # Re-initialise state dict for the new param objects. + from collections import defaultdict + + inner_optimizer.state = defaultdict(dict) + + self._inner = inner_optimizer + self.max_grad_norm = max_grad_norm + + # Call torch.optim.Optimizer.__init__ so that isinstance(self, Optimizer) + # returns True. The _initializing flag prevents add_param_group from + # delegating to self._inner during super().__init__ (which would + # create a duplicate param group in the inner optimizer). + self._initializing = True + super().__init__(self._fp32_params, inner_optimizer.defaults) + self._initializing = False + + # Redirect param_groups and state to inner optimizer's versions so + # lr_scheduler / lr logging always see the correct param groups. + self.param_groups = self._inner.param_groups + self.state = self._inner.state + + # ------------------------------------------------------------------ # + # Core step / zero_grad — called directly by HF Trainer # + # ------------------------------------------------------------------ # + + def step(self, closure=None): + """Full fp32 master-weight update step.""" + with torch.no_grad(): + # (a) Copy bf16 grads -> fp32 master grads. + for bf16_p, fp32_p in zip(self._bf16_params, self._fp32_params): + if bf16_p.grad is not None: + fp32_p.grad = bf16_p.grad.detach().to(torch.float32) + bf16_p.grad = None + else: + fp32_p.grad = None + + # (b) Clip fp32 grad norm. + if self.max_grad_norm > 0: + torch.nn.utils.clip_grad_norm_(self._fp32_params, self.max_grad_norm) + + # (c) Optimizer step on fp32 params. + loss = self._inner.step(closure) + + # (d) Copy fp32 -> bf16 model params. + with torch.no_grad(): + for bf16_p, fp32_p in zip(self._bf16_params, self._fp32_params): + bf16_p.data.copy_(fp32_p.data.to(bf16_p.dtype)) + + return loss + + def zero_grad(self, set_to_none: bool = True): + """Zero gradients on both bf16 model params and fp32 master params.""" + for bf16_p in self._bf16_params: + if set_to_none: + bf16_p.grad = None + elif bf16_p.grad is not None: + bf16_p.grad.zero_() + for fp32_p in self._fp32_params: + if set_to_none: + fp32_p.grad = None + elif fp32_p.grad is not None: + fp32_p.grad.zero_() + + # ------------------------------------------------------------------ # + # Delegate everything else to the inner optimizer # + # ------------------------------------------------------------------ # + + def state_dict(self): + return self._inner.state_dict() + + def load_state_dict(self, state_dict): + return self._inner.load_state_dict(state_dict) + + def add_param_group(self, param_group): + # During super().__init__ the inner optimizer is not yet assigned, + # so fall back to the default Optimizer behaviour. + if getattr(self, "_initializing", True): + return super().add_param_group(param_group) + return self._inner.add_param_group(param_group) + + def __repr__(self): + return f"_FP32MasterWeightOptimizer({self._inner})" + + @Eagle3TrainerFactory.register("online", "DFlash") class OnlineDFlashTrainer(Eagle3Trainer): """Online DFlash Trainer for speculative decoding training. @@ -244,12 +495,32 @@ def __init__( self.block_size = getattr(draft_model_config, "block_size", 16) self.num_anchors = getattr(draft_model_config, "num_anchors", 512) self.loss_decay_gamma = getattr(draft_model_config, "loss_decay_gamma", None) - self.attention_backend = getattr(draft_model_config, "attention_backend", "flex_attention") + # Gamma warmup: gradually increase loss_decay_gamma per epoch + # (AngelSlim DFlash gamma-warmup schedule). + self._gamma_init = self.loss_decay_gamma + self.gamma_warmup = getattr(draft_model_config, "gamma_warmup", False) + self._gamma_step = getattr(draft_model_config, "gamma_warmup_step", 0.5) + self.attention_backend = getattr( + draft_model_config, "attention_backend", "flex_attention" + ) self.mask_token_id = dflash_config.get( "mask_token_id", getattr(draft_model_config, "mask_token_id", None), ) + # Sync _attn_implementation on the draft model so its attention layers + # dispatch to the correct backend (eager vs flex_attention vs sdpa). + if self.attention_backend == "eager": + draft_model.config._attn_implementation = "eager" + elif self.attention_backend == "flex_attention": + draft_model.config._attn_implementation = "flex_attention" + else: + draft_model.config._attn_implementation = self.attention_backend + + # fp32 master weights optimizer — set by create_optimizer() (DDP path). + # FSDP path uses _FP32StateAdamW directly as self.optimizer instead. + self._fp32_optimizer: Optional["_FP32MasterWeightOptimizer"] = None + # Load target model's lm_head and embed_tokens # In offline mode target_model may be None; fall back to config path. target_model_path = None @@ -279,6 +550,171 @@ def __init__( "or target_model.model_path for DFlash training." ) + def create_optimizer(self, model=None): + """Create optimizer for DFlash training. + + Three branches: + * DeepSpeed: defer to HF Trainer's default optimizer creation. + * FSDP: AdamW with fp32 optimizer states (``_FP32StateAdamW``), + using the AngelSlim DFlash fp32-master pattern. Critical because + bf16 momentum and variance only have 7-bit mantissa, which causes + training quality degradation after a few thousand steps. + * DDP / single GPU: ``_FP32MasterWeightOptimizer`` wrapping AdamW for + fp32 master weight updates. + """ + if self.is_deepspeed_enabled: + return super().create_optimizer(model) + + if self.is_fsdp_enabled: + args = self.args + param_groups = [ + {"params": [p for p in self.model.parameters() if p.requires_grad]} + ] + optimizer = _FP32StateAdamW( + param_groups, + lr=args.learning_rate, + betas=( + getattr(args, "adam_beta1", 0.9), + getattr(args, "adam_beta2", 0.999), + ), + eps=getattr(args, "adam_epsilon", 1e-8), + weight_decay=args.weight_decay, + max_grad_norm=args.max_grad_norm, + ) + self.optimizer = optimizer + return self.optimizer + + bf16_params: List[torch.Tensor] = [ + p for p in self.model.parameters() if p.requires_grad + ] + if not bf16_params: + return super().create_optimizer(model) + + from torch.optim import AdamW + + args = self.args + inner_optimizer = AdamW( + # Placeholder — _FP32MasterWeightOptimizer will replace param_groups + # with fp32 copies immediately after construction. + bf16_params, + lr=args.learning_rate, + betas=( + getattr(args, "adam_beta1", 0.9), + getattr(args, "adam_beta2", 0.999), + ), + eps=getattr(args, "adam_epsilon", 1e-8), + weight_decay=args.weight_decay, + ) + + fp32_opt = _FP32MasterWeightOptimizer( + bf16_params=bf16_params, + inner_optimizer=inner_optimizer, + max_grad_norm=args.max_grad_norm, + ) + self._fp32_optimizer = fp32_opt + self.optimizer = fp32_opt + return self.optimizer + + def create_scheduler(self, num_training_steps: int, optimizer=None): + """Create LR scheduler: AngelSlim DFlash CosineAnnealingWarmupLR. + + AngelSlim warmup formula: lr = base_lr * (step + 1) / warmup_steps + HF Trainer warmup formula: lr = base_lr * step / warmup_steps + + The +1 offset means step 0 yields lr = base_lr / warmup_steps instead + of 0. After warmup, both use identical cosine annealing. + """ + import math + + from torch.optim.lr_scheduler import LambdaLR + + if optimizer is None: + optimizer = self.optimizer + + warmup_steps = self.args.get_warmup_steps(num_training_steps) + + def angelslim_cosine_schedule(current_step: int) -> float: + """LR multiplier for AngelSlim DFlash CosineAnnealingWarmupLR.""" + if current_step < warmup_steps: + # AngelSlim: (last_epoch + 1) / warmup_epochs * base_lr + # After N step() calls last_epoch = N, so first lr = 1/warmup_steps. + return float(current_step + 1) / float(max(1, warmup_steps)) + # Cosine decay phase — identical to HF. + progress = float(current_step - warmup_steps) / float( + max(1, num_training_steps - warmup_steps) + ) + return max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress))) + + self.lr_scheduler = LambdaLR(optimizer, angelslim_cosine_schedule) + return self.lr_scheduler + + def _clip_grad_norm(self, *args, **kwargs): + """Skip HF Trainer's built-in grad clipping when running our fp32 optimizers. + + Both ``_FP32MasterWeightOptimizer`` (DDP) and ``_FP32StateAdamW`` (FSDP) + clip gradients internally on fp32 grads (AngelSlim DFlash fp32-master + clipping). HF Trainer's Accelerator-based clip_grad_norm_ would + otherwise run on bf16 grads (incorrect precision for clipping), causing + DOUBLE CLIPPING that significantly slows down training. + """ + if self._fp32_optimizer is not None: + # DDP path — clipped inside _FP32MasterWeightOptimizer.step() + return torch.tensor(0.0) + + # FSDP path: _FP32StateAdamW clips internally on fp32 grads. + # self.optimizer may be wrapped by AcceleratedOptimizer, so unwrap once. + optimizer = self.optimizer + if hasattr(optimizer, "optimizer"): + optimizer = optimizer.optimizer + if isinstance(optimizer, _FP32StateAdamW): + return torch.tensor(0.0) + + return super()._clip_grad_norm(*args, **kwargs) + + def save_optimizer_and_scheduler(self, output_dir, **kwargs): + """Override to handle fp32 master weight optimizer with FSDP. + + FSDP's built-in optim_state_dict() cannot handle our custom fp32 + master-weight optimizers because their fp32 params are not registered + in the FSDP module's parameter graph. Save optimizer/scheduler state + directly instead. + """ + self._save_optimizer_and_scheduler(output_dir) + + def _save_optimizer_and_scheduler(self, output_dir): + """Bypass FSDP's optim_state_dict for our custom fp32 optimizers.""" + optimizer = self.optimizer + if hasattr(optimizer, "optimizer"): + # Unwrap AcceleratedOptimizer + optimizer = optimizer.optimizer + + if isinstance(optimizer, (_FP32StateAdamW, _FP32MasterWeightOptimizer)): + if self.args.should_save: + torch.save( + optimizer.state_dict(), + os.path.join(output_dir, "optimizer.pt"), + ) + if self.lr_scheduler is not None: + torch.save( + self.lr_scheduler.state_dict(), + os.path.join(output_dir, "scheduler.pt"), + ) + else: + super()._save_optimizer_and_scheduler(output_dir) + + def _update_gamma_warmup(self): + """Update loss_decay_gamma: gamma = gamma_init + step * epoch. + + AngelSlim DFlash gamma-warmup schedule: + current_gamma = loss_decay_gamma + step * float(epoch) + """ + if not self.gamma_warmup or self._gamma_init is None: + return + current_epoch = int(self.state.epoch) if hasattr(self.state, "epoch") else 0 + self.loss_decay_gamma = self._gamma_init + self._gamma_step * float( + current_epoch + ) + def prepare_data_for_draft_model(self, inputs): """Prepare data for DFlash training. @@ -501,6 +937,9 @@ def compute_loss( Unlike Eagle3's iterative multi-step loss, DFlash computes a single block-parallel cross-entropy loss over all sampled anchor positions. """ + # Update gamma if warmup is enabled (no-op when gamma_warmup=False) + self._update_gamma_warmup() + data = self.prepare_data_for_draft_model(inputs) loss, accuracy = self._compute_dflash_loss_and_accuracy( diff --git a/configs/fsdp_config.json b/configs/fsdp_config.json new file mode 100644 index 00000000..9721e1ae --- /dev/null +++ b/configs/fsdp_config.json @@ -0,0 +1,6 @@ +{ + "fsdp_sharding_strategy": "SHARD_GRAD_OP", + "fsdp_auto_wrap_policy": "NO_WRAP", + "fsdp_backward_prefetch": "backward_pre", + "fsdp_use_orig_params": true +} diff --git a/configs/qwen3_dflare.json b/configs/qwen3_dflare.json new file mode 100644 index 00000000..5378f418 --- /dev/null +++ b/configs/qwen3_dflare.json @@ -0,0 +1,60 @@ +{ + "architectures": [ + "QwenDFlareDraftModel" + ], + "attention_bias": false, + "attention_dropout": 0.0, + "block_size": 16, + "bos_token_id": 151643, + "dflash_config": { + "mask_token_id": 151669, + "target_layer_ids": [ + 1, + 5, + 9, + 13, + 17, + 21, + 25, + 29, + 33 + ] + }, + "dtype": "bfloat16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2560, + "initializer_range": 0.02, + "intermediate_size": 9728, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "max_position_embeddings": 40960, + "max_window_layers": 7, + "model_type": "qwen3", + "num_attention_heads": 32, + "num_hidden_layers": 7, + "num_key_value_heads": 8, + "num_target_layers": 36, + "rms_norm_eps": 1e-06, + "rope_scaling": null, + "rope_theta": 1000000, + "sliding_window": null, + "tie_word_embeddings": true, + "torch_dtype": "bfloat16", + "transformers_version": "4.57.3", + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 151936, + + "num_anchors": 512, + "loss_decay_gamma": 7.0, + "attention_backend": "flex_attention" +} diff --git a/scripts/speculative/run_dflare_online.sh b/scripts/speculative/run_dflare_online.sh new file mode 100755 index 00000000..63ae59b4 --- /dev/null +++ b/scripts/speculative/run_dflare_online.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# DFlare Online Training Script for Qwen3 +# Usage: bash scripts/speculative/run_dflare_online.sh [NUM_GPUS] [ATTENTION_BACKEND] + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) + +# Use local source code instead of installed site-packages +export PYTHONPATH=$ROOT_DIR:$PYTHONPATH + +NUM_GPUS=${1:-8} +ATTENTION_BACKEND=${2:-flex_attention} + +# Set paths - modify these to match your environment +TARGET_MODEL_PATH="/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B" +TRAIN_DATA_PATH="/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl" +OUTPUT_DIR="${ROOT_DIR}/outputs/" + +export CONFIG_DIR=${ROOT_DIR}/angelslim/compressor/speculative/train/configs + +# WandB configuration +export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflare"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflare"} + +torchrun \ + --standalone \ + --nproc_per_node $NUM_GPUS \ + $ROOT_DIR/tools/train_dflash_online.py \ + --target_model_name_or_path $TARGET_MODEL_PATH \ + --draft_model_config_path $ROOT_DIR/configs/qwen3_dflare.json \ + --draft_arch dflare \ + --train_data_path $TRAIN_DATA_PATH \ + --output_dir $OUTPUT_DIR \ + --modal_type DFlash \ + --training_mode online \ + --num_train_epochs 6 \ + --per_device_train_batch_size 2 \ + --learning_rate 6e-4 \ + --warmup_ratio 0.04 \ + --max_grad_norm 1.0 \ + --model_max_length 3072 \ + --chat_template_type qwen3 \ + --attention_backend $ATTENTION_BACKEND \ + --block_size 16 \ + --num_anchors 512 \ + --loss_decay_gamma 7.0 \ + --logging_steps 50 \ + --save_strategy steps \ + --save_steps 2500 \ + --bf16 \ + --lr_scheduler_type cosine \ + --report_to wandb \ + --run_name $WANDB_RUN_NAME + diff --git a/scripts/speculative/run_dflash_aligned.sh b/scripts/speculative/run_dflash_aligned.sh new file mode 100755 index 00000000..3ddcba98 --- /dev/null +++ b/scripts/speculative/run_dflash_aligned.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# ========================================================================== +# AngelSlim DFlash Online Training — Fully Aligned Configuration +# ========================================================================== +# +# Recommended training entry for DFlash. Enables all AngelSlim DFlash +# alignment features: +# +# - loss_decay_gamma: 7 (fixed by default; pass --gamma_warmup to enable +# per-epoch increment via --gamma_warmup_step). +# - block_size: 16, num_anchors: 512. +# - batch_size: 2, lr: 6e-4, cosine schedule, warmup_ratio: 0.04. +# - max_length: 3072, num_epochs: 6. +# - num_proc: 64 for data preprocessing. +# - Target model uses flash_attention_2 (matches the inference kernel and +# avoids train/test attention-backend mismatch). +# - dataloader_drop_last=True (avoids FSDP shape mismatches on the +# trailing batch). +# - FP32 master weights optimizer (fp32 accumulation + fp32 grad clip; +# only the final copy-back introduces bf16 quantization). +# - FSDP shard_grad_op + auto_wrap (with configs/fsdp_config.json: +# NO_WRAP, use_orig_params=True). +# +# Usage: +# bash scripts/speculative/run_dflash_aligned.sh [NUM_GPUS] [ATTENTION_BACKEND] +# +# ========================================================================== + +set -euo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) + +# Use local source code instead of installed site-packages +export PYTHONPATH=$ROOT_DIR:${PYTHONPATH:-} + +NUM_GPUS=${1:-8} +ATTENTION_BACKEND=${2:-flex_attention} + +# ========================================================================== +# Paths — modify to match your environment +# ========================================================================== +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-"/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B"} +DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflash.json"} +TRAIN_DATA_PATH=${TRAIN_DATA_PATH:-"/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl"} +OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflash-aligned"} + +# ========================================================================== +# torch.compile / inductor kernel cache +# ========================================================================== +export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-${ROOT_DIR}/cache/compiled_kernels} + +# ========================================================================== +# Data preprocessing parallelism +# ========================================================================== +DATA_NUM_PROC=${DATA_NUM_PROC:-64} + +# ========================================================================== +# WandB configuration +# ========================================================================== +export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"angelslim-qwen3-4b-dflash-fp32master-aligned"} + +# ========================================================================== +# Multi-node configuration (optional) +# ========================================================================== +NNODES=${NNODES:-1} +NODE_RANK=${NODE_RANK:-0} +MASTER_ADDR=${MASTER_ADDR:-"localhost"} +MASTER_PORT=${MASTER_PORT:-12347} + +if [ "$NNODES" -gt 1 ]; then + DISTRIBUTED_ARGS="--nproc_per_node $NUM_GPUS --nnodes=$NNODES --node_rank=$NODE_RANK --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT" + echo "[INFO] Multi-node training: nnodes=$NNODES, node_rank=$NODE_RANK, master=$MASTER_ADDR:$MASTER_PORT" +else + DISTRIBUTED_ARGS="--standalone --nproc_per_node $NUM_GPUS" + echo "[INFO] Single-node training: $NUM_GPUS GPUs" +fi + +# ========================================================================== +# NCCL multi-node communication (for H20 + RoCE 400Gbps); harmless on single node +# ========================================================================== +if [ "$NNODES" -gt 1 ]; then + export NCCL_IB_DISABLE=0 + export NCCL_SOCKET_IFNAME=bond1 + export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_2,mlx5_bond_3,mlx5_bond_4,mlx5_bond_5,mlx5_bond_6,mlx5_bond_7,mlx5_bond_8 + export NCCL_IB_GID_INDEX=3 + export NCCL_IB_TIMEOUT=23 + export NCCL_IB_RETRY_CNT=7 + export NCCL_NET_GDR_LEVEL=2 + export NCCL_IB_QPS_PER_CONNECTION=4 + export NCCL_CROSS_NIC=1 + export NCCL_ALGO=Ring + export NCCL_PROTO=Simple + export NCCL_DEBUG=${NCCL_DEBUG:-INFO} + export CUDA_DEVICE_MAX_CONNECTIONS=1 + export NCCL_TIMEOUT=1800 +fi + +echo "[INFO] Draft config: $DRAFT_CONFIG_PATH" +echo "[INFO] Target model: $TARGET_MODEL_PATH" +echo "[INFO] Train data: $TRAIN_DATA_PATH" +echo "[INFO] Output dir: $OUTPUT_DIR" +echo "[INFO] Attention backend (draft): $ATTENTION_BACKEND" +echo "[INFO] Target model attn: flash_attention_2 (set in target_model_wrapper.py)" +echo "[INFO] WandB project: $WANDB_PROJECT, run: $WANDB_RUN_NAME" +echo "" + +# ========================================================================== +# Launch training +# ========================================================================== +torchrun $DISTRIBUTED_ARGS \ + $ROOT_DIR/tools/train_dflash_online.py \ + --target_model_name_or_path $TARGET_MODEL_PATH \ + --draft_model_config_path $DRAFT_CONFIG_PATH \ + --train_data_path $TRAIN_DATA_PATH \ + --output_dir $OUTPUT_DIR \ + --modal_type DFlash \ + --training_mode online \ + --num_train_epochs 6 \ + --per_device_train_batch_size 2 \ + --learning_rate 6e-4 \ + --warmup_ratio 0.04 \ + --max_grad_norm 1.0 \ + --model_max_length 3072 \ + --chat_template_type qwen3 \ + --attention_backend $ATTENTION_BACKEND \ + --block_size 16 \ + --num_anchors 512 \ + --loss_decay_gamma 7 \ + --num_proc $DATA_NUM_PROC \ + --logging_steps 50 \ + --save_strategy steps \ + --save_steps 5000 \ + --bf16 \ + --lr_scheduler_type cosine \ + --dataloader_drop_last \ + --fsdp "shard_grad_op auto_wrap" \ + --fsdp_config ${ROOT_DIR}/configs/fsdp_config.json \ + --report_to wandb \ + --wandb_project $WANDB_PROJECT \ + --wandb_run_name $WANDB_RUN_NAME diff --git a/scripts/speculative/run_dflash_online.sh b/scripts/speculative/run_dflash_online.sh index 1d4a1397..1eab5d1c 100644 --- a/scripts/speculative/run_dflash_online.sh +++ b/scripts/speculative/run_dflash_online.sh @@ -13,13 +13,13 @@ NUM_GPUS=${1:-8} ATTENTION_BACKEND=${2:-flex_attention} # Set paths - modify these to match your environment -TARGET_MODEL_PATH="" -TRAIN_DATA_PATH="" +TARGET_MODEL_PATH="/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B" +TRAIN_DATA_PATH="/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl" OUTPUT_DIR="${ROOT_DIR}/outputs/" export CONFIG_DIR=${ROOT_DIR}/angelslim/compressor/speculative/train/configs -# WandB configuration (mirrors SpecForge's --wandb-project / --wandb-name) +# WandB configuration export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflash"} diff --git a/tools/train_dflash_online.py b/tools/train_dflash_online.py index 5a33a16d..097495ae 100755 --- a/tools/train_dflash_online.py +++ b/tools/train_dflash_online.py @@ -93,6 +93,17 @@ def parse_args(): default="model.embed_tokens.weight", help="Key for embedding weights in model config", ) + model_group.add_argument( + "--draft_arch", + type=str, + default=None, + choices=["dflash", "dflare"], + help=( + "Override draft model architecture. If unset, uses the " + "'architectures' field from the draft_model_config JSON. " + "'dflash' -> QwenDFlashDraftModel, 'dflare' -> QwenDFlareDraftModel." + ), + ) # DFlash-specific arguments dflash_group = parser.add_argument_group("DFlash Arguments") @@ -266,6 +277,45 @@ def parse_args(): ) training_group.add_argument("--fp16", action="store_true", help="Whether to use fp16 training") training_group.add_argument("--bf16", action="store_true", help="Whether to use bf16 training") + training_group.add_argument( + "--fsdp", + type=str, + default="", + help="FSDP configuration string passed to TrainingArguments " + "(e.g. 'shard_grad_op auto_wrap'). Empty disables FSDP.", + ) + training_group.add_argument( + "--fsdp_config", + type=str, + default=None, + help="Path to FSDP config JSON file (consumed by TrainingArguments).", + ) + training_group.add_argument( + "--dataloader_drop_last", + action="store_true", + default=False, + help=( + "Drop last incomplete batch. Note: when using DFlash trainer this " + "is forced True internally to match AngelSlim's drop_last=True " + "and avoid FSDP shape mismatches on the trailing batch." + ), + ) + training_group.add_argument( + "--gamma_warmup", + action="store_true", + default=False, + help=( + "Enable gamma warmup. When set, loss_decay_gamma is increased per " + "epoch as: gamma = loss_decay_gamma + gamma_warmup_step * epoch " + "(AngelSlim gamma warmup formula)." + ), + ) + training_group.add_argument( + "--gamma_warmup_step", + type=float, + default=0.5, + help="Per-epoch increment for gamma warmup. Default 0.5.", + ) training_group.add_argument( "--save_strategy", type=str, default="no", help="Save strategy for checkpoints" ) @@ -286,7 +336,7 @@ def parse_args(): help="The list of integrations to report the results and logs to (e.g. 'wandb')", ) - # WandB arguments (mirrors SpecForge's --wandb-project / --wandb-name) + # WandB arguments wandb_group = parser.add_argument_group("WandB Arguments") wandb_group.add_argument( "--wandb_project", @@ -307,7 +357,7 @@ def parse_args(): def _setup_wandb(args) -> None: """Set up WandB environment variables and initialize wandb run on rank 0. - Mirrors the --wandb-project / --wandb-name pattern from SpecForge. + Sets up WandB project / run name from CLI or env vars. Priority: CLI args > env vars > defaults. """ if args.report_to not in ("wandb", "all"): @@ -355,6 +405,22 @@ def train(): draft_model_config = DraftModelConfig.from_file(args.draft_model_config_path) target_model_type = getattr(draft_model_config, "target_model_type", None) + # Optionally override draft architecture from CLI. Both DFlash and DFlare + # share the same Qwen3Config schema (block_size, dflash_config, etc.), so + # swapping the architectures field is sufficient to route create_draft_model + # to the desired class via DraftModelFactory._get_model_class. + if args.draft_arch is not None: + arch_map = { + "dflash": "QwenDFlashDraftModel", + "dflare": "QwenDFlareDraftModel", + } + new_arch = arch_map[args.draft_arch] + rank0_print( + f"Overriding draft architecture: " + f"{getattr(draft_model_config, 'architectures', None)} -> [{new_arch}]" + ) + draft_model_config.architectures = [new_arch] + # Inject DFlash-specific config into the draft model config # so the trainer can access them draft_model_config.target_model_name_or_path = args.target_model_name_or_path @@ -368,6 +434,10 @@ def train(): draft_model_config.num_anchors = args.num_anchors if args.loss_decay_gamma is not None: draft_model_config.loss_decay_gamma = args.loss_decay_gamma + # Always propagate gamma_warmup flags to the draft model config so the + # trainer can pick them up regardless of CLI defaults. + draft_model_config.gamma_warmup = args.gamma_warmup + draft_model_config.gamma_warmup_step = args.gamma_warmup_step if args.attention_backend is not None: draft_model_config.attention_backend = args.attention_backend if args.mask_token_id is not None: @@ -442,6 +512,10 @@ def train(): "per_device_eval_batch_size": args.per_device_eval_batch_size, "gradient_accumulation_steps": args.gradient_accumulation_steps, "remove_unused_columns": False, + # Force drop_last=True (AngelSlim default) to avoid FSDP shape + # mismatches on the trailing batch. CLI --dataloader_drop_last is + # accepted for compatibility but currently overridden here. + "dataloader_drop_last": True, } optimizer_args = { @@ -475,7 +549,10 @@ def train(): distributed_args = { "deepspeed": args.deepspeed, + "fsdp": args.fsdp, } + if args.fsdp_config: + distributed_args["fsdp_config"] = args.fsdp_config training_args = transformers.TrainingArguments( **basic_args, From e50dbf698f32553be8043df07efa2f2b4e5b53e1 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Thu, 28 May 2026 15:53:04 +0800 Subject: [PATCH 15/23] dflare --- scripts/speculative/run_dflash_offline.sh | 136 ++++++++++++++++++---- scripts/speculative/run_dflash_online.sh | 123 ++++++++++++++++--- tools/train_dflash_offline.py | 79 ++++++++++++- 3 files changed, 300 insertions(+), 38 deletions(-) diff --git a/scripts/speculative/run_dflash_offline.sh b/scripts/speculative/run_dflash_offline.sh index 05524719..b8a2cfe5 100644 --- a/scripts/speculative/run_dflash_offline.sh +++ b/scripts/speculative/run_dflash_offline.sh @@ -1,38 +1,130 @@ #!/bin/bash -# ============================================================================= -# Step 2: Train DFlash draft model in OFFLINE mode. + +# ========================================================================== +# AngelSlim DFlash Offline Training — Fully Aligned Configuration +# ========================================================================== +# +# Trains a DFlash draft model from pre-computed hidden-state .ckpt files. +# Prerequisite: run scripts/speculative/generate_dflash_data.sh first to +# produce the .ckpt files at $TRAIN_HIDDEN_PATH. +# +# Enables all AngelSlim DFlash alignment features (same as the online entry, +# minus the on-the-fly target-model forward): # -# Prerequisites: -# Run generate_qwen3_dflash_data.sh first to produce the .ckpt files. +# - block_size: 16, num_anchors: 512. +# - batch_size: 2, lr: 6e-4, cosine schedule, warmup_ratio: 0.04. +# - max_length: 3072. +# - dataloader_drop_last=True (avoids FSDP shape mismatches on the +# trailing batch). +# - FP32 master weights optimizer (fp32 accumulation + fp32 grad clip; +# only the final copy-back introduces bf16 quantization). +# - FSDP shard_grad_op + auto_wrap (with configs/fsdp_config.json: +# NO_WRAP, use_orig_params=True). +# - loss_decay_gamma: 7 (fixed by default; pass --gamma_warmup to enable +# per-epoch increment via --gamma_warmup_step). # # Usage: -# bash scripts/speculative/run_qwen3_dflash_offline.sh [NUM_GPUS] -# ============================================================================= +# bash scripts/speculative/run_dflash_offline.sh [NUM_GPUS] [ATTENTION_BACKEND] +# +# ========================================================================== + +set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) # Use local source code instead of installed site-packages -export PYTHONPATH=$ROOT_DIR:$PYTHONPATH +export PYTHONPATH=$ROOT_DIR:${PYTHONPATH:-} NUM_GPUS=${1:-8} +ATTENTION_BACKEND=${2:-flex_attention} + +# ========================================================================== +# Paths — modify to match your environment +# ========================================================================== +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-"/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B"} +DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflash.json"} +TRAIN_HIDDEN_PATH=${TRAIN_HIDDEN_PATH:-""} +EVAL_HIDDEN_PATH=${EVAL_HIDDEN_PATH:-""} +OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflash-offline"} -# ---- Paths -- modify these to match your environment ---- -TARGET_MODEL_PATH="" -TRAIN_HIDDEN_PATH="" -OUTPUT_DIR="${ROOT_DIR}/outputs/" +if [ -z "$TRAIN_HIDDEN_PATH" ]; then + echo "[ERROR] TRAIN_HIDDEN_PATH is empty. Set it to the directory holding " + echo " pre-computed .ckpt files (output of generate_dflash_data.sh)." + exit 1 +fi +# ========================================================================== +# torch.compile / inductor kernel cache +# ========================================================================== +export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-${ROOT_DIR}/cache/compiled_kernels} + +# ========================================================================== # WandB configuration +# ========================================================================== export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} -WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflash-offline"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"angelslim-qwen3-4b-dflash-offline-fp32master"} + +# ========================================================================== +# Multi-node configuration (optional) +# ========================================================================== +NNODES=${NNODES:-1} +NODE_RANK=${NODE_RANK:-0} +MASTER_ADDR=${MASTER_ADDR:-"localhost"} +MASTER_PORT=${MASTER_PORT:-12347} + +if [ "$NNODES" -gt 1 ]; then + DISTRIBUTED_ARGS="--nproc_per_node $NUM_GPUS --nnodes=$NNODES --node_rank=$NODE_RANK --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT" + echo "[INFO] Multi-node training: nnodes=$NNODES, node_rank=$NODE_RANK, master=$MASTER_ADDR:$MASTER_PORT" +else + DISTRIBUTED_ARGS="--standalone --nproc_per_node $NUM_GPUS" + echo "[INFO] Single-node training: $NUM_GPUS GPUs" +fi + +# ========================================================================== +# NCCL multi-node communication (for H20 + RoCE 400Gbps); harmless on single node +# ========================================================================== +if [ "$NNODES" -gt 1 ]; then + export NCCL_IB_DISABLE=0 + export NCCL_SOCKET_IFNAME=bond1 + export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_2,mlx5_bond_3,mlx5_bond_4,mlx5_bond_5,mlx5_bond_6,mlx5_bond_7,mlx5_bond_8 + export NCCL_IB_GID_INDEX=3 + export NCCL_IB_TIMEOUT=23 + export NCCL_IB_RETRY_CNT=7 + export NCCL_NET_GDR_LEVEL=2 + export NCCL_IB_QPS_PER_CONNECTION=4 + export NCCL_CROSS_NIC=1 + export NCCL_ALGO=Ring + export NCCL_PROTO=Simple + export NCCL_DEBUG=${NCCL_DEBUG:-INFO} + export CUDA_DEVICE_MAX_CONNECTIONS=1 + export NCCL_TIMEOUT=1800 +fi + +# Optional eval-data argument (only added if path provided) +EVAL_FLAGS="" +if [ -n "$EVAL_HIDDEN_PATH" ]; then + EVAL_FLAGS="--eval_hidden_path $EVAL_HIDDEN_PATH" +fi + +echo "[INFO] Draft config: $DRAFT_CONFIG_PATH" +echo "[INFO] Target model: $TARGET_MODEL_PATH" +echo "[INFO] Train hidden path: $TRAIN_HIDDEN_PATH" +echo "[INFO] Eval hidden path: ${EVAL_HIDDEN_PATH:-}" +echo "[INFO] Output dir: $OUTPUT_DIR" +echo "[INFO] Attention backend (draft): $ATTENTION_BACKEND" +echo "[INFO] WandB project: $WANDB_PROJECT, run: $WANDB_RUN_NAME" +echo "" -torchrun \ - --standalone \ - --nproc_per_node $NUM_GPUS \ +# ========================================================================== +# Launch training +# ========================================================================== +torchrun $DISTRIBUTED_ARGS \ $ROOT_DIR/tools/train_dflash_offline.py \ --target_model_name_or_path $TARGET_MODEL_PATH \ - --draft_model_config_path $ROOT_DIR/configs/qwen3_dflash.json \ + --draft_model_config_path $DRAFT_CONFIG_PATH \ --train_hidden_path $TRAIN_HIDDEN_PATH \ + $EVAL_FLAGS \ --output_dir $OUTPUT_DIR \ --num_train_epochs 12 \ --per_device_train_batch_size 2 \ @@ -41,14 +133,18 @@ torchrun \ --max_grad_norm 1.0 \ --model_max_length 3072 \ --chat_template_type qwen3 \ - --attention_backend flex_attention \ + --attention_backend $ATTENTION_BACKEND \ --block_size 16 \ --num_anchors 512 \ - --loss_decay_gamma 7.0 \ + --loss_decay_gamma 7 \ --logging_steps 50 \ --save_strategy steps \ - --save_steps 2500 \ + --save_steps 5000 \ --bf16 \ --lr_scheduler_type cosine \ + --dataloader_drop_last \ + --fsdp "shard_grad_op auto_wrap" \ + --fsdp_config ${ROOT_DIR}/configs/fsdp_config.json \ --report_to wandb \ - --run_name $WANDB_RUN_NAME + --wandb_project $WANDB_PROJECT \ + --wandb_run_name $WANDB_RUN_NAME diff --git a/scripts/speculative/run_dflash_online.sh b/scripts/speculative/run_dflash_online.sh index 1eab5d1c..a5d8b9ba 100644 --- a/scripts/speculative/run_dflash_online.sh +++ b/scripts/speculative/run_dflash_online.sh @@ -1,34 +1,119 @@ #!/bin/bash -# DFlash Online Training Script for Qwen3 -# Usage: bash scripts/speculative/run_qwen3_dflash_online.sh [NUM_GPUS] [ATTENTION_BACKEND] +# ========================================================================== +# AngelSlim DFlash Online Training — Fully Aligned Configuration +# ========================================================================== +# +# Recommended training entry for DFlash. Enables all AngelSlim DFlash +# alignment features: +# +# - loss_decay_gamma: 7 (fixed by default; pass --gamma_warmup to enable +# per-epoch increment via --gamma_warmup_step). +# - block_size: 16, num_anchors: 512. +# - batch_size: 2, lr: 6e-4, cosine schedule, warmup_ratio: 0.04. +# - max_length: 3072, num_epochs: 6. +# - num_proc: 64 for data preprocessing. +# - Target model uses flash_attention_2 (matches the inference kernel and +# avoids train/test attention-backend mismatch). +# - dataloader_drop_last=True (avoids FSDP shape mismatches on the +# trailing batch). +# - FP32 master weights optimizer (fp32 accumulation + fp32 grad clip; +# only the final copy-back introduces bf16 quantization). +# - FSDP shard_grad_op + auto_wrap (with configs/fsdp_config.json: +# NO_WRAP, use_orig_params=True). +# +# Usage: +# bash scripts/speculative/run_dflash_online.sh [NUM_GPUS] [ATTENTION_BACKEND] +# +# ========================================================================== + +set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) # Use local source code instead of installed site-packages -export PYTHONPATH=$ROOT_DIR:$PYTHONPATH +export PYTHONPATH=$ROOT_DIR:${PYTHONPATH:-} NUM_GPUS=${1:-8} ATTENTION_BACKEND=${2:-flex_attention} -# Set paths - modify these to match your environment -TARGET_MODEL_PATH="/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B" -TRAIN_DATA_PATH="/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl" -OUTPUT_DIR="${ROOT_DIR}/outputs/" +# ========================================================================== +# Paths — modify to match your environment +# ========================================================================== +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-"/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B"} +DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflash.json"} +TRAIN_DATA_PATH=${TRAIN_DATA_PATH:-"/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl"} +OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflash-online"} + +# ========================================================================== +# torch.compile / inductor kernel cache +# ========================================================================== +export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-${ROOT_DIR}/cache/compiled_kernels} -export CONFIG_DIR=${ROOT_DIR}/angelslim/compressor/speculative/train/configs +# ========================================================================== +# Data preprocessing parallelism +# ========================================================================== +DATA_NUM_PROC=${DATA_NUM_PROC:-64} +# ========================================================================== # WandB configuration +# ========================================================================== export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} -WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflash"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"angelslim-qwen3-4b-dflash-online-fp32master"} + +# ========================================================================== +# Multi-node configuration (optional) +# ========================================================================== +NNODES=${NNODES:-1} +NODE_RANK=${NODE_RANK:-0} +MASTER_ADDR=${MASTER_ADDR:-"localhost"} +MASTER_PORT=${MASTER_PORT:-12347} + +if [ "$NNODES" -gt 1 ]; then + DISTRIBUTED_ARGS="--nproc_per_node $NUM_GPUS --nnodes=$NNODES --node_rank=$NODE_RANK --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT" + echo "[INFO] Multi-node training: nnodes=$NNODES, node_rank=$NODE_RANK, master=$MASTER_ADDR:$MASTER_PORT" +else + DISTRIBUTED_ARGS="--standalone --nproc_per_node $NUM_GPUS" + echo "[INFO] Single-node training: $NUM_GPUS GPUs" +fi -torchrun \ - --standalone \ - --nproc_per_node $NUM_GPUS \ +# ========================================================================== +# NCCL multi-node communication (for H20 + RoCE 400Gbps); harmless on single node +# ========================================================================== +if [ "$NNODES" -gt 1 ]; then + export NCCL_IB_DISABLE=0 + export NCCL_SOCKET_IFNAME=bond1 + export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_2,mlx5_bond_3,mlx5_bond_4,mlx5_bond_5,mlx5_bond_6,mlx5_bond_7,mlx5_bond_8 + export NCCL_IB_GID_INDEX=3 + export NCCL_IB_TIMEOUT=23 + export NCCL_IB_RETRY_CNT=7 + export NCCL_NET_GDR_LEVEL=2 + export NCCL_IB_QPS_PER_CONNECTION=4 + export NCCL_CROSS_NIC=1 + export NCCL_ALGO=Ring + export NCCL_PROTO=Simple + export NCCL_DEBUG=${NCCL_DEBUG:-INFO} + export CUDA_DEVICE_MAX_CONNECTIONS=1 + export NCCL_TIMEOUT=1800 +fi + +echo "[INFO] Draft config: $DRAFT_CONFIG_PATH" +echo "[INFO] Target model: $TARGET_MODEL_PATH" +echo "[INFO] Train data: $TRAIN_DATA_PATH" +echo "[INFO] Output dir: $OUTPUT_DIR" +echo "[INFO] Attention backend (draft): $ATTENTION_BACKEND" +echo "[INFO] Target model attn: flash_attention_2 (set in target_model_wrapper.py)" +echo "[INFO] WandB project: $WANDB_PROJECT, run: $WANDB_RUN_NAME" +echo "" + +# ========================================================================== +# Launch training +# ========================================================================== +torchrun $DISTRIBUTED_ARGS \ $ROOT_DIR/tools/train_dflash_online.py \ --target_model_name_or_path $TARGET_MODEL_PATH \ - --draft_model_config_path $ROOT_DIR/configs/qwen3_dflash.json \ + --draft_model_config_path $DRAFT_CONFIG_PATH \ --train_data_path $TRAIN_DATA_PATH \ --output_dir $OUTPUT_DIR \ --modal_type DFlash \ @@ -43,12 +128,16 @@ torchrun \ --attention_backend $ATTENTION_BACKEND \ --block_size 16 \ --num_anchors 512 \ - --loss_decay_gamma 7.0 \ + --loss_decay_gamma 7 \ + --num_proc $DATA_NUM_PROC \ --logging_steps 50 \ --save_strategy steps \ - --save_steps 2500 \ + --save_steps 5000 \ --bf16 \ --lr_scheduler_type cosine \ + --dataloader_drop_last \ + --fsdp "shard_grad_op auto_wrap" \ + --fsdp_config ${ROOT_DIR}/configs/fsdp_config.json \ --report_to wandb \ - --run_name $WANDB_RUN_NAME - + --wandb_project $WANDB_PROJECT \ + --wandb_run_name $WANDB_RUN_NAME diff --git a/tools/train_dflash_offline.py b/tools/train_dflash_offline.py index 4f8ad528..d2dd32d2 100755 --- a/tools/train_dflash_offline.py +++ b/tools/train_dflash_offline.py @@ -97,6 +97,17 @@ def parse_args(): m.add_argument("--trust_remote_code", action="store_true", default=True) m.add_argument("--embed_weight_key", type=str, default="model.embed_tokens.weight") m.add_argument("--lm_head_key", type=str, default="lm_head.weight") + m.add_argument( + "--draft_arch", + type=str, + default=None, + choices=["dflash", "dflare"], + help=( + "Override draft model architecture. If unset, uses the " + "'architectures' field from the draft_model_config JSON. " + "'dflash' -> QwenDFlashDraftModel, 'dflare' -> QwenDFlareDraftModel." + ), + ) # DFlash-specific (override values in config JSON) d = parser.add_argument_group("DFlash Arguments") @@ -160,6 +171,44 @@ def parse_args(): t.add_argument("--fp16", action="store_true") t.add_argument("--bf16", action="store_true") t.add_argument("--deepspeed", type=str, default=None) + t.add_argument( + "--fsdp", + type=str, + default="", + help="FSDP configuration string passed to TrainingArguments " + "(e.g. 'shard_grad_op auto_wrap'). Empty disables FSDP.", + ) + t.add_argument( + "--fsdp_config", + type=str, + default=None, + help="Path to FSDP config JSON file (consumed by TrainingArguments).", + ) + t.add_argument( + "--dataloader_drop_last", + action="store_true", + default=False, + help=( + "Drop last incomplete batch. Note: when using DFlash trainer this " + "is forced True internally to avoid FSDP shape mismatches on the " + "trailing batch." + ), + ) + t.add_argument( + "--gamma_warmup", + action="store_true", + default=False, + help=( + "Enable gamma warmup. When set, loss_decay_gamma is increased " + "per epoch as: gamma = loss_decay_gamma + gamma_warmup_step * epoch." + ), + ) + t.add_argument( + "--gamma_warmup_step", + type=float, + default=0.5, + help="Per-epoch increment for gamma warmup. Default 0.5.", + ) t.add_argument("--report_to", type=str, default="none") t.add_argument("--run_name", type=str, default=None) t.add_argument("--training_time_test_length", type=int, default=7) @@ -216,6 +265,22 @@ def train(): draft_model_config.embed_weight_key = args.embed_weight_key draft_model_config.trust_remote_code = args.trust_remote_code + # Optionally override draft architecture from CLI. Both DFlash and DFlare + # share the same Qwen3Config schema (block_size, dflash_config, etc.), so + # swapping the architectures field is sufficient to route create_draft_model + # to the desired class. + if args.draft_arch is not None: + arch_map = { + "dflash": "QwenDFlashDraftModel", + "dflare": "QwenDFlareDraftModel", + } + new_arch = arch_map[args.draft_arch] + rank0_print( + f"Overriding draft architecture: " + f"{getattr(draft_model_config, 'architectures', None)} -> [{new_arch}]" + ) + draft_model_config.architectures = [new_arch] + # Override DFlash params from CLI if specified if args.block_size is not None: draft_model_config.block_size = args.block_size @@ -223,6 +288,10 @@ def train(): draft_model_config.num_anchors = args.num_anchors if args.loss_decay_gamma is not None: draft_model_config.loss_decay_gamma = args.loss_decay_gamma + # Always propagate gamma_warmup flags to the draft model config so the + # trainer can pick them up regardless of CLI defaults. + draft_model_config.gamma_warmup = args.gamma_warmup + draft_model_config.gamma_warmup_step = args.gamma_warmup_step if args.attention_backend is not None: draft_model_config.attention_backend = args.attention_backend draft_model_config._attn_implementation = args.attention_backend @@ -264,7 +333,7 @@ def train(): # ------------------------------------------------------------------ # 4. TrainingArguments # ------------------------------------------------------------------ - training_args = transformers.TrainingArguments( + ta_kwargs = dict( output_dir=args.output_dir, num_train_epochs=args.num_train_epochs, per_device_train_batch_size=args.per_device_train_batch_size, @@ -288,8 +357,16 @@ def train(): report_to=args.report_to, run_name=args.run_name, deepspeed=args.deepspeed, + fsdp=args.fsdp, + # Force drop_last=True (AngelSlim default) to avoid FSDP shape + # mismatches on the trailing batch. + dataloader_drop_last=True, remove_unused_columns=False, ) + if args.fsdp_config: + ta_kwargs["fsdp_config"] = args.fsdp_config + + training_args = transformers.TrainingArguments(**ta_kwargs) # ------------------------------------------------------------------ # 5. Trainer -- use Eagle3TrainerFactory From be207f6a722db1dec50475981cd212ab031362b6 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Fri, 29 May 2026 12:04:21 +0800 Subject: [PATCH 16/23] dflare --- scripts/speculative/run_dflare_offline.sh | 165 ++++++++++++++++++++++ scripts/speculative/run_dflare_online.sh | 135 +++++++++++++++--- scripts/speculative/run_dflash_offline.sh | 8 +- scripts/speculative/run_dflash_online.sh | 15 +- 4 files changed, 301 insertions(+), 22 deletions(-) create mode 100755 scripts/speculative/run_dflare_offline.sh diff --git a/scripts/speculative/run_dflare_offline.sh b/scripts/speculative/run_dflare_offline.sh new file mode 100755 index 00000000..f4a44d50 --- /dev/null +++ b/scripts/speculative/run_dflare_offline.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# ========================================================================== +# AngelSlim DFlare Offline Training — Fully Aligned Configuration +# ========================================================================== +# +# Trains a DFlare draft model from pre-computed hidden-state .ckpt files. +# DFlare is the enhanced DFlash variant with separate context/noise k/v +# projections and learnable per-layer fusion weights. Training-side logic +# is identical to DFlash, so this script reuses tools/train_dflash_offline.py +# and selects DFlare via --draft_arch. +# +# Prerequisite: run scripts/speculative/generate_dflash_data.sh with a +# DFlare-compatible draft config first to produce the .ckpt files at +# $TRAIN_HIDDEN_PATH. Specifically the .ckpt's hidden_states must contain +# the SAME number of target layers that the DFlare config expects (i.e. +# len(dflash_config.target_layer_ids) in qwen3_dflare.json). +# +# Enables all AngelSlim DFlash alignment features (same as the online entry, +# minus the on-the-fly target-model forward): +# +# - block_size: 16, num_anchors: 512. +# - batch_size: 2, lr: 6e-4, cosine schedule, warmup_ratio: 0.04. +# - max_length: 3072. +# - dataloader_drop_last=True (avoids FSDP shape mismatches on the +# trailing batch). +# - FP32 master weights optimizer (fp32 accumulation + fp32 grad clip; +# only the final copy-back introduces bf16 quantization). +# - FSDP shard_grad_op + auto_wrap (with configs/fsdp_config.json: +# NO_WRAP, use_orig_params=True). +# - loss_decay_gamma: 7 (fixed by default; pass --gamma_warmup to enable +# per-epoch increment via --gamma_warmup_step). +# +# Usage: +# bash scripts/speculative/run_dflare_offline.sh [NUM_GPUS] [ATTENTION_BACKEND] +# +# ========================================================================== + +set -euo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) + +# Use local source code instead of installed site-packages +export PYTHONPATH=$ROOT_DIR:${PYTHONPATH:-} + +NUM_GPUS=${1:-8} +ATTENTION_BACKEND=${2:-flex_attention} + +# ========================================================================== +# Paths — set these before running (left empty by default for portability) +# ========================================================================== +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-""} +DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflare.json"} +TRAIN_HIDDEN_PATH=${TRAIN_HIDDEN_PATH:-""} +EVAL_HIDDEN_PATH=${EVAL_HIDDEN_PATH:-""} +OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflare-offline"} + +if [ -z "$TARGET_MODEL_PATH" ]; then + echo "[ERROR] TARGET_MODEL_PATH is empty. Export it to your local Qwen3 (or other) HF model dir." + exit 1 +fi +if [ -z "$TRAIN_HIDDEN_PATH" ]; then + echo "[ERROR] TRAIN_HIDDEN_PATH is empty. Set it to the directory holding " + echo " pre-computed .ckpt files (output of generate_dflash_data.sh" + echo " run with a DFlare-compatible draft config so target_layer_ids match)." + exit 1 +fi + +# ========================================================================== +# torch.compile / inductor kernel cache +# ========================================================================== +export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-${ROOT_DIR}/cache/compiled_kernels} + +# ========================================================================== +# WandB configuration +# ========================================================================== +export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflare"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"angelslim-qwen3-4b-dflare-offline-fp32master"} + +# ========================================================================== +# Multi-node configuration (optional) +# ========================================================================== +NNODES=${NNODES:-1} +NODE_RANK=${NODE_RANK:-0} +MASTER_ADDR=${MASTER_ADDR:-"localhost"} +MASTER_PORT=${MASTER_PORT:-12347} + +if [ "$NNODES" -gt 1 ]; then + DISTRIBUTED_ARGS="--nproc_per_node $NUM_GPUS --nnodes=$NNODES --node_rank=$NODE_RANK --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT" + echo "[INFO] Multi-node training: nnodes=$NNODES, node_rank=$NODE_RANK, master=$MASTER_ADDR:$MASTER_PORT" +else + DISTRIBUTED_ARGS="--standalone --nproc_per_node $NUM_GPUS" + echo "[INFO] Single-node training: $NUM_GPUS GPUs" +fi + +# ========================================================================== +# NCCL multi-node communication (for H20 + RoCE 400Gbps); harmless on single node +# ========================================================================== +if [ "$NNODES" -gt 1 ]; then + export NCCL_IB_DISABLE=0 + export NCCL_SOCKET_IFNAME=bond1 + export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_2,mlx5_bond_3,mlx5_bond_4,mlx5_bond_5,mlx5_bond_6,mlx5_bond_7,mlx5_bond_8 + export NCCL_IB_GID_INDEX=3 + export NCCL_IB_TIMEOUT=23 + export NCCL_IB_RETRY_CNT=7 + export NCCL_NET_GDR_LEVEL=2 + export NCCL_IB_QPS_PER_CONNECTION=4 + export NCCL_CROSS_NIC=1 + export NCCL_ALGO=Ring + export NCCL_PROTO=Simple + export NCCL_DEBUG=${NCCL_DEBUG:-INFO} + export CUDA_DEVICE_MAX_CONNECTIONS=1 + export NCCL_TIMEOUT=1800 +fi + +# Optional eval-data argument (only added if path provided) +EVAL_FLAGS="" +if [ -n "$EVAL_HIDDEN_PATH" ]; then + EVAL_FLAGS="--eval_hidden_path $EVAL_HIDDEN_PATH" +fi + +echo "[INFO] Draft config: $DRAFT_CONFIG_PATH" +echo "[INFO] Target model: $TARGET_MODEL_PATH" +echo "[INFO] Train hidden path: $TRAIN_HIDDEN_PATH" +echo "[INFO] Eval hidden path: ${EVAL_HIDDEN_PATH:-}" +echo "[INFO] Output dir: $OUTPUT_DIR" +echo "[INFO] Attention backend (draft): $ATTENTION_BACKEND" +echo "[INFO] Draft architecture: dflare (--draft_arch dflare)" +echo "[INFO] WandB project: $WANDB_PROJECT, run: $WANDB_RUN_NAME" +echo "" + +# ========================================================================== +# Launch training +# ========================================================================== +torchrun $DISTRIBUTED_ARGS \ + $ROOT_DIR/tools/train_dflash_offline.py \ + --target_model_name_or_path $TARGET_MODEL_PATH \ + --draft_model_config_path $DRAFT_CONFIG_PATH \ + --draft_arch dflare \ + --train_hidden_path $TRAIN_HIDDEN_PATH \ + $EVAL_FLAGS \ + --output_dir $OUTPUT_DIR \ + --num_train_epochs 12 \ + --per_device_train_batch_size 2 \ + --learning_rate 6e-4 \ + --warmup_ratio 0.04 \ + --max_grad_norm 1.0 \ + --model_max_length 3072 \ + --chat_template_type qwen3 \ + --attention_backend $ATTENTION_BACKEND \ + --block_size 16 \ + --num_anchors 512 \ + --loss_decay_gamma 7 \ + --logging_steps 50 \ + --save_strategy steps \ + --save_steps 5000 \ + --bf16 \ + --lr_scheduler_type cosine \ + --dataloader_drop_last \ + --fsdp "shard_grad_op auto_wrap" \ + --fsdp_config ${ROOT_DIR}/configs/fsdp_config.json \ + --report_to wandb \ + --wandb_project $WANDB_PROJECT \ + --wandb_run_name $WANDB_RUN_NAME diff --git a/scripts/speculative/run_dflare_online.sh b/scripts/speculative/run_dflare_online.sh index 63ae59b4..c0ee041a 100755 --- a/scripts/speculative/run_dflare_online.sh +++ b/scripts/speculative/run_dflare_online.sh @@ -1,34 +1,131 @@ #!/bin/bash -# DFlare Online Training Script for Qwen3 -# Usage: bash scripts/speculative/run_dflare_online.sh [NUM_GPUS] [ATTENTION_BACKEND] +# ========================================================================== +# AngelSlim DFlare Online Training — Fully Aligned Configuration +# ========================================================================== +# +# Recommended training entry for DFlare. DFlare is the enhanced DFlash +# variant with separate context/noise k/v projections and learnable +# per-layer fusion weights. Training-side logic is identical to DFlash, so +# all alignment features below apply unchanged: +# +# - loss_decay_gamma: 7 (fixed by default; pass --gamma_warmup to enable +# per-epoch increment via --gamma_warmup_step). +# - block_size: 16, num_anchors: 512. +# - batch_size: 2, lr: 6e-4, cosine schedule, warmup_ratio: 0.04. +# - max_length: 3072, num_epochs: 6. +# - num_proc: 64 for data preprocessing. +# - Target model uses flash_attention_2 (matches the inference kernel and +# avoids train/test attention-backend mismatch). +# - dataloader_drop_last=True (avoids FSDP shape mismatches on the +# trailing batch). +# - FP32 master weights optimizer (fp32 accumulation + fp32 grad clip; +# only the final copy-back introduces bf16 quantization). +# - FSDP shard_grad_op + auto_wrap (with configs/fsdp_config.json: +# NO_WRAP, use_orig_params=True). +# +# Usage: +# bash scripts/speculative/run_dflare_online.sh [NUM_GPUS] [ATTENTION_BACKEND] +# +# ========================================================================== + +set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) # Use local source code instead of installed site-packages -export PYTHONPATH=$ROOT_DIR:$PYTHONPATH +export PYTHONPATH=$ROOT_DIR:${PYTHONPATH:-} NUM_GPUS=${1:-8} ATTENTION_BACKEND=${2:-flex_attention} -# Set paths - modify these to match your environment -TARGET_MODEL_PATH="/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B" -TRAIN_DATA_PATH="/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl" -OUTPUT_DIR="${ROOT_DIR}/outputs/" +# ========================================================================== +# Paths — set these before running (left empty by default for portability) +# ========================================================================== +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-""} +DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflare.json"} +TRAIN_DATA_PATH=${TRAIN_DATA_PATH:-""} +OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflare-aligned"} + +if [ -z "$TARGET_MODEL_PATH" ]; then + echo "[ERROR] TARGET_MODEL_PATH is empty. Export it to your local Qwen3 (or other) HF model dir." + exit 1 +fi +if [ -z "$TRAIN_DATA_PATH" ]; then + echo "[ERROR] TRAIN_DATA_PATH is empty. Export it to a JSON/JSONL conversation dataset file." + exit 1 +fi -export CONFIG_DIR=${ROOT_DIR}/angelslim/compressor/speculative/train/configs +# ========================================================================== +# torch.compile / inductor kernel cache +# ========================================================================== +export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-${ROOT_DIR}/cache/compiled_kernels} +# ========================================================================== +# Data preprocessing parallelism +# ========================================================================== +DATA_NUM_PROC=${DATA_NUM_PROC:-64} + +# ========================================================================== # WandB configuration +# ========================================================================== export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflare"} -WANDB_RUN_NAME=${WANDB_RUN_NAME:-"qwen3-4b-dflare"} +WANDB_RUN_NAME=${WANDB_RUN_NAME:-"angelslim-qwen3-4b-dflare-fp32master-aligned"} + +# ========================================================================== +# Multi-node configuration (optional) +# ========================================================================== +NNODES=${NNODES:-1} +NODE_RANK=${NODE_RANK:-0} +MASTER_ADDR=${MASTER_ADDR:-"localhost"} +MASTER_PORT=${MASTER_PORT:-12347} -torchrun \ - --standalone \ - --nproc_per_node $NUM_GPUS \ +if [ "$NNODES" -gt 1 ]; then + DISTRIBUTED_ARGS="--nproc_per_node $NUM_GPUS --nnodes=$NNODES --node_rank=$NODE_RANK --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT" + echo "[INFO] Multi-node training: nnodes=$NNODES, node_rank=$NODE_RANK, master=$MASTER_ADDR:$MASTER_PORT" +else + DISTRIBUTED_ARGS="--standalone --nproc_per_node $NUM_GPUS" + echo "[INFO] Single-node training: $NUM_GPUS GPUs" +fi + +# ========================================================================== +# NCCL multi-node communication (for H20 + RoCE 400Gbps); harmless on single node +# ========================================================================== +if [ "$NNODES" -gt 1 ]; then + export NCCL_IB_DISABLE=0 + export NCCL_SOCKET_IFNAME=bond1 + export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_2,mlx5_bond_3,mlx5_bond_4,mlx5_bond_5,mlx5_bond_6,mlx5_bond_7,mlx5_bond_8 + export NCCL_IB_GID_INDEX=3 + export NCCL_IB_TIMEOUT=23 + export NCCL_IB_RETRY_CNT=7 + export NCCL_NET_GDR_LEVEL=2 + export NCCL_IB_QPS_PER_CONNECTION=4 + export NCCL_CROSS_NIC=1 + export NCCL_ALGO=Ring + export NCCL_PROTO=Simple + export NCCL_DEBUG=${NCCL_DEBUG:-INFO} + export CUDA_DEVICE_MAX_CONNECTIONS=1 + export NCCL_TIMEOUT=1800 +fi + +echo "[INFO] Draft config: $DRAFT_CONFIG_PATH" +echo "[INFO] Target model: $TARGET_MODEL_PATH" +echo "[INFO] Train data: $TRAIN_DATA_PATH" +echo "[INFO] Output dir: $OUTPUT_DIR" +echo "[INFO] Attention backend (draft): $ATTENTION_BACKEND" +echo "[INFO] Target model attn: flash_attention_2 (set in target_model_wrapper.py)" +echo "[INFO] Draft architecture: dflare (--draft_arch dflare)" +echo "[INFO] WandB project: $WANDB_PROJECT, run: $WANDB_RUN_NAME" +echo "" + +# ========================================================================== +# Launch training +# ========================================================================== +torchrun $DISTRIBUTED_ARGS \ $ROOT_DIR/tools/train_dflash_online.py \ --target_model_name_or_path $TARGET_MODEL_PATH \ - --draft_model_config_path $ROOT_DIR/configs/qwen3_dflare.json \ + --draft_model_config_path $DRAFT_CONFIG_PATH \ --draft_arch dflare \ --train_data_path $TRAIN_DATA_PATH \ --output_dir $OUTPUT_DIR \ @@ -44,12 +141,16 @@ torchrun \ --attention_backend $ATTENTION_BACKEND \ --block_size 16 \ --num_anchors 512 \ - --loss_decay_gamma 7.0 \ + --loss_decay_gamma 7 \ + --num_proc $DATA_NUM_PROC \ --logging_steps 50 \ --save_strategy steps \ - --save_steps 2500 \ + --save_steps 5000 \ --bf16 \ --lr_scheduler_type cosine \ + --dataloader_drop_last \ + --fsdp "shard_grad_op auto_wrap" \ + --fsdp_config ${ROOT_DIR}/configs/fsdp_config.json \ --report_to wandb \ - --run_name $WANDB_RUN_NAME - + --wandb_project $WANDB_PROJECT \ + --wandb_run_name $WANDB_RUN_NAME diff --git a/scripts/speculative/run_dflash_offline.sh b/scripts/speculative/run_dflash_offline.sh index b8a2cfe5..8cee87b1 100644 --- a/scripts/speculative/run_dflash_offline.sh +++ b/scripts/speculative/run_dflash_offline.sh @@ -40,14 +40,18 @@ NUM_GPUS=${1:-8} ATTENTION_BACKEND=${2:-flex_attention} # ========================================================================== -# Paths — modify to match your environment +# Paths — set these before running (left empty by default for portability) # ========================================================================== -TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-"/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B"} +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-""} DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflash.json"} TRAIN_HIDDEN_PATH=${TRAIN_HIDDEN_PATH:-""} EVAL_HIDDEN_PATH=${EVAL_HIDDEN_PATH:-""} OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflash-offline"} +if [ -z "$TARGET_MODEL_PATH" ]; then + echo "[ERROR] TARGET_MODEL_PATH is empty. Export it to your local Qwen3 (or other) HF model dir." + exit 1 +fi if [ -z "$TRAIN_HIDDEN_PATH" ]; then echo "[ERROR] TRAIN_HIDDEN_PATH is empty. Set it to the directory holding " echo " pre-computed .ckpt files (output of generate_dflash_data.sh)." diff --git a/scripts/speculative/run_dflash_online.sh b/scripts/speculative/run_dflash_online.sh index a5d8b9ba..62d3f4ae 100644 --- a/scripts/speculative/run_dflash_online.sh +++ b/scripts/speculative/run_dflash_online.sh @@ -39,13 +39,22 @@ NUM_GPUS=${1:-8} ATTENTION_BACKEND=${2:-flex_attention} # ========================================================================== -# Paths — modify to match your environment +# Paths — set these before running (left empty by default for portability) # ========================================================================== -TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-"/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B"} +TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-""} DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflash.json"} -TRAIN_DATA_PATH=${TRAIN_DATA_PATH:-"/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl"} +TRAIN_DATA_PATH=${TRAIN_DATA_PATH:-""} OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflash-online"} +if [ -z "$TARGET_MODEL_PATH" ]; then + echo "[ERROR] TARGET_MODEL_PATH is empty. Export it to your local Qwen3 (or other) HF model dir." + exit 1 +fi +if [ -z "$TRAIN_DATA_PATH" ]; then + echo "[ERROR] TRAIN_DATA_PATH is empty. Export it to a JSON/JSONL conversation dataset file." + exit 1 +fi + # ========================================================================== # torch.compile / inductor kernel cache # ========================================================================== From 0d7b19b51295b5715107aed246caa5de081334f6 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Sat, 30 May 2026 16:46:40 +0800 Subject: [PATCH 17/23] remove unused run_dflash_aligned.sh --- scripts/speculative/run_dflash_aligned.sh | 143 ---------------------- 1 file changed, 143 deletions(-) delete mode 100755 scripts/speculative/run_dflash_aligned.sh diff --git a/scripts/speculative/run_dflash_aligned.sh b/scripts/speculative/run_dflash_aligned.sh deleted file mode 100755 index 3ddcba98..00000000 --- a/scripts/speculative/run_dflash_aligned.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash - -# ========================================================================== -# AngelSlim DFlash Online Training — Fully Aligned Configuration -# ========================================================================== -# -# Recommended training entry for DFlash. Enables all AngelSlim DFlash -# alignment features: -# -# - loss_decay_gamma: 7 (fixed by default; pass --gamma_warmup to enable -# per-epoch increment via --gamma_warmup_step). -# - block_size: 16, num_anchors: 512. -# - batch_size: 2, lr: 6e-4, cosine schedule, warmup_ratio: 0.04. -# - max_length: 3072, num_epochs: 6. -# - num_proc: 64 for data preprocessing. -# - Target model uses flash_attention_2 (matches the inference kernel and -# avoids train/test attention-backend mismatch). -# - dataloader_drop_last=True (avoids FSDP shape mismatches on the -# trailing batch). -# - FP32 master weights optimizer (fp32 accumulation + fp32 grad clip; -# only the final copy-back introduces bf16 quantization). -# - FSDP shard_grad_op + auto_wrap (with configs/fsdp_config.json: -# NO_WRAP, use_orig_params=True). -# -# Usage: -# bash scripts/speculative/run_dflash_aligned.sh [NUM_GPUS] [ATTENTION_BACKEND] -# -# ========================================================================== - -set -euo pipefail - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR)) - -# Use local source code instead of installed site-packages -export PYTHONPATH=$ROOT_DIR:${PYTHONPATH:-} - -NUM_GPUS=${1:-8} -ATTENTION_BACKEND=${2:-flex_attention} - -# ========================================================================== -# Paths — modify to match your environment -# ========================================================================== -TARGET_MODEL_PATH=${TARGET_MODEL_PATH:-"/apdcephfs_gy5_303770945/share_303770945/jiebin/hf_models/Qwen/Qwen3-4B"} -DRAFT_CONFIG_PATH=${DRAFT_CONFIG_PATH:-"${ROOT_DIR}/configs/qwen3_dflash.json"} -TRAIN_DATA_PATH=${TRAIN_DATA_PATH:-"/cfs_cloud_code/jiebinzhang/SpecForge/cache/dataset/codealpaca-20k_train.jsonl"} -OUTPUT_DIR=${OUTPUT_DIR:-"${ROOT_DIR}/outputs/qwen3-4b-dflash-aligned"} - -# ========================================================================== -# torch.compile / inductor kernel cache -# ========================================================================== -export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-${ROOT_DIR}/cache/compiled_kernels} - -# ========================================================================== -# Data preprocessing parallelism -# ========================================================================== -DATA_NUM_PROC=${DATA_NUM_PROC:-64} - -# ========================================================================== -# WandB configuration -# ========================================================================== -export WANDB_PROJECT=${WANDB_PROJECT:-"angelslim-qwen3-4b-dflash"} -WANDB_RUN_NAME=${WANDB_RUN_NAME:-"angelslim-qwen3-4b-dflash-fp32master-aligned"} - -# ========================================================================== -# Multi-node configuration (optional) -# ========================================================================== -NNODES=${NNODES:-1} -NODE_RANK=${NODE_RANK:-0} -MASTER_ADDR=${MASTER_ADDR:-"localhost"} -MASTER_PORT=${MASTER_PORT:-12347} - -if [ "$NNODES" -gt 1 ]; then - DISTRIBUTED_ARGS="--nproc_per_node $NUM_GPUS --nnodes=$NNODES --node_rank=$NODE_RANK --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT" - echo "[INFO] Multi-node training: nnodes=$NNODES, node_rank=$NODE_RANK, master=$MASTER_ADDR:$MASTER_PORT" -else - DISTRIBUTED_ARGS="--standalone --nproc_per_node $NUM_GPUS" - echo "[INFO] Single-node training: $NUM_GPUS GPUs" -fi - -# ========================================================================== -# NCCL multi-node communication (for H20 + RoCE 400Gbps); harmless on single node -# ========================================================================== -if [ "$NNODES" -gt 1 ]; then - export NCCL_IB_DISABLE=0 - export NCCL_SOCKET_IFNAME=bond1 - export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_2,mlx5_bond_3,mlx5_bond_4,mlx5_bond_5,mlx5_bond_6,mlx5_bond_7,mlx5_bond_8 - export NCCL_IB_GID_INDEX=3 - export NCCL_IB_TIMEOUT=23 - export NCCL_IB_RETRY_CNT=7 - export NCCL_NET_GDR_LEVEL=2 - export NCCL_IB_QPS_PER_CONNECTION=4 - export NCCL_CROSS_NIC=1 - export NCCL_ALGO=Ring - export NCCL_PROTO=Simple - export NCCL_DEBUG=${NCCL_DEBUG:-INFO} - export CUDA_DEVICE_MAX_CONNECTIONS=1 - export NCCL_TIMEOUT=1800 -fi - -echo "[INFO] Draft config: $DRAFT_CONFIG_PATH" -echo "[INFO] Target model: $TARGET_MODEL_PATH" -echo "[INFO] Train data: $TRAIN_DATA_PATH" -echo "[INFO] Output dir: $OUTPUT_DIR" -echo "[INFO] Attention backend (draft): $ATTENTION_BACKEND" -echo "[INFO] Target model attn: flash_attention_2 (set in target_model_wrapper.py)" -echo "[INFO] WandB project: $WANDB_PROJECT, run: $WANDB_RUN_NAME" -echo "" - -# ========================================================================== -# Launch training -# ========================================================================== -torchrun $DISTRIBUTED_ARGS \ - $ROOT_DIR/tools/train_dflash_online.py \ - --target_model_name_or_path $TARGET_MODEL_PATH \ - --draft_model_config_path $DRAFT_CONFIG_PATH \ - --train_data_path $TRAIN_DATA_PATH \ - --output_dir $OUTPUT_DIR \ - --modal_type DFlash \ - --training_mode online \ - --num_train_epochs 6 \ - --per_device_train_batch_size 2 \ - --learning_rate 6e-4 \ - --warmup_ratio 0.04 \ - --max_grad_norm 1.0 \ - --model_max_length 3072 \ - --chat_template_type qwen3 \ - --attention_backend $ATTENTION_BACKEND \ - --block_size 16 \ - --num_anchors 512 \ - --loss_decay_gamma 7 \ - --num_proc $DATA_NUM_PROC \ - --logging_steps 50 \ - --save_strategy steps \ - --save_steps 5000 \ - --bf16 \ - --lr_scheduler_type cosine \ - --dataloader_drop_last \ - --fsdp "shard_grad_op auto_wrap" \ - --fsdp_config ${ROOT_DIR}/configs/fsdp_config.json \ - --report_to wandb \ - --wandb_project $WANDB_PROJECT \ - --wandb_run_name $WANDB_RUN_NAME From 6071780aa6306f32e5b6b27d8d857a73daaf9b4d Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Sat, 30 May 2026 16:56:24 +0800 Subject: [PATCH 18/23] dflare --- .../train/models/draft/qwen_dflare.py | 66 +++++-------------- .../models/target/target_model_wrapper.py | 14 ++-- .../train/trainer/online_dflash_trainer.py | 25 +++---- tools/train_dflash_offline.py | 2 +- tools/train_dflash_online.py | 2 +- 5 files changed, 32 insertions(+), 77 deletions(-) diff --git a/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py b/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py index 5905b43f..51b32912 100644 --- a/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py +++ b/angelslim/compressor/speculative/train/models/draft/qwen_dflare.py @@ -72,9 +72,7 @@ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): return q_embed, k_embed -def build_target_layer_ids( - num_target_layers: int, num_draft_layers: int -) -> List[int]: +def build_target_layer_ids(num_target_layers: int, num_draft_layers: int) -> List[int]: """Compute target layer IDs to capture from the target model.""" if num_draft_layers == 1: return [(num_target_layers // 2)] @@ -82,8 +80,7 @@ def build_target_layer_ids( end = num_target_layers - 3 span = end - start target_layer_ids = [ - int(round(start + (i * span) / (num_draft_layers - 1))) - for i in range(num_draft_layers) + int(round(start + (i * span) / (num_draft_layers - 1))) for i in range(num_draft_layers) ] return target_layer_ids @@ -116,9 +113,7 @@ def __init__(self, config: Qwen3Config, layer_idx: int): self.head_dim = getattr( config, "head_dim", config.hidden_size // config.num_attention_heads ) - self.num_key_value_groups = ( - config.num_attention_heads // config.num_key_value_heads - ) + self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads self.scaling = self.head_dim**-0.5 self.attention_dropout = config.attention_dropout self.is_causal = False @@ -155,9 +150,7 @@ def __init__(self, config: Qwen3Config, layer_idx: int): self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps) self.sliding_window = ( - config.sliding_window - if config.layer_types[layer_idx] == "sliding_attention" - else None + config.sliding_window if config.layer_types[layer_idx] == "sliding_attention" else None ) def forward( @@ -179,12 +172,8 @@ def forward( k_noise = self.k_proj(hidden_states) v_ctx = self.v_proj_target(target_hidden) v_noise = self.v_proj(hidden_states) - k = torch.cat([k_ctx, k_noise], dim=1).view( - bsz, ctx_len + q_len, -1, self.head_dim - ) - v = torch.cat([v_ctx, v_noise], dim=1).view( - bsz, ctx_len + q_len, -1, self.head_dim - ) + k = torch.cat([k_ctx, k_noise], dim=1).view(bsz, ctx_len + q_len, -1, self.head_dim) + v = torch.cat([v_ctx, v_noise], dim=1).view(bsz, ctx_len + q_len, -1, self.head_dim) k = self.k_norm(k).transpose(1, 2) v = v.transpose(1, 2) cos, sin = position_embeddings @@ -220,9 +209,7 @@ def __init__(self, config: Qwen3Config, layer_idx: int): self.self_attn = Qwen3DFlareAttention(config=config, layer_idx=layer_idx) self.mlp = Qwen3MLP(config) self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) - self.post_attention_layernorm = Qwen3RMSNorm( - config.hidden_size, eps=config.rms_norm_eps - ) + self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) def forward( self, @@ -234,9 +221,7 @@ def forward( output_attentions: Optional[bool] = False, use_cache: Optional[bool] = False, cache_position: Optional[torch.LongTensor] = None, - position_embeddings: Optional[ - Tuple[torch.Tensor, torch.Tensor] - ] = None, + position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, **kwargs: Unpack[FlashAttentionKwargs], ) -> torch.FloatTensor: residual = hidden_states @@ -332,9 +317,7 @@ def forward( ) fusion_probs = torch.softmax(self.layer_fusion_weights, dim=1) # bsth (target) x dt (per-draft-layer fusion) -> bsdh (per-draft-layer) - fused_hidden = torch.einsum( - "bsth,dt->bsdh", target_hidden_reshaped, fusion_probs - ) + fused_hidden = torch.einsum("bsth,dt->bsdh", target_hidden_reshaped, fusion_probs) fused_hidden = self.hidden_norm(fused_hidden) position_embeddings = self.rotary_emb(hidden_states, position_ids) for i, layer in enumerate(self.layers): @@ -372,9 +355,7 @@ def spec_generate( dtype=torch.long, device=target.device, ) - position_ids = torch.arange( - output_ids.shape[1], device=target.device - ).unsqueeze(0) + position_ids = torch.arange(output_ids.shape[1], device=target.device).unsqueeze(0) past_key_values_target = DynamicCache() past_key_values_draft = DynamicCache() @@ -390,12 +371,8 @@ def spec_generate( ) output_ids[:, :num_input_tokens] = input_ids - output_ids[:, num_input_tokens : num_input_tokens + 1] = sample( - output.logits, temperature - ) - target_hidden = extract_context_feature( - output.hidden_states, self.target_layer_ids - ) + output_ids[:, num_input_tokens : num_input_tokens + 1] = sample(output.logits, temperature) + target_hidden = extract_context_feature(output.hidden_states, self.target_layer_ids) # Decode stage acceptance_lengths = [] @@ -429,22 +406,17 @@ def spec_generate( posterior = sample(output.logits, temperature) acceptance_length = ( - (block_output_ids[:, 1:] == posterior[:, :-1]) - .cumprod(dim=1) - .sum(dim=1)[0] - .item() + (block_output_ids[:, 1:] == posterior[:, :-1]).cumprod(dim=1).sum(dim=1)[0].item() ) output_ids[:, start : start + acceptance_length + 1] = block_output_ids[ :, : acceptance_length + 1 ] - output_ids[:, start + acceptance_length + 1] = posterior[ - :, acceptance_length - ] + output_ids[:, start + acceptance_length + 1] = posterior[:, acceptance_length] start += acceptance_length + 1 past_key_values_target.crop(start) - target_hidden = extract_context_feature( - output.hidden_states, self.target_layer_ids - )[:, : acceptance_length + 1, :] + target_hidden = extract_context_feature(output.hidden_states, self.target_layer_ids)[ + :, : acceptance_length + 1, : + ] acceptance_lengths.append(acceptance_length + 1) if stop_token_ids is not None and any( stop_token_id in output_ids[:, num_input_tokens:] @@ -459,8 +431,6 @@ def spec_generate( output_ids[0][num_input_tokens:], stop_token_ids ).nonzero(as_tuple=True)[0] if stop_token_indices.numel() > 0: - output_ids = output_ids[ - :, : num_input_tokens + stop_token_indices[0] + 1 - ] + output_ids = output_ids[:, : num_input_tokens + stop_token_indices[0] + 1] return output_ids diff --git a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py index b8438be9..b7bba384 100644 --- a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py +++ b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py @@ -185,9 +185,7 @@ def load_model(self) -> None: # Verify attention implementation actually used _actual_attn = getattr(self.model.config, "_attn_implementation", "unknown") - print_with_rank( - f"Target model loaded. Actual attn_implementation: {_actual_attn}" - ) + print_with_rank(f"Target model loaded. Actual attn_implementation: {_actual_attn}") # Load tokenizer self.tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True) @@ -263,7 +261,7 @@ def get_hidden_states_and_logits( actual_len = seq_len # Extract the unpadded portion - single_ids = input_ids[i:i + 1, :actual_len] + single_ids = input_ids[i : i + 1, :actual_len] with torch.no_grad(): outputs = self.model( @@ -274,18 +272,14 @@ def get_hidden_states_and_logits( # Extract auxiliary hidden states for this sample # h shape: [1, actual_len, D*num_layers] - h = self._extract_auxiliary_hidden_states( - outputs.hidden_states, aux_layer_ids - ) + h = self._extract_auxiliary_hidden_states(outputs.hidden_states, aux_layer_ids) # Pad back to seq_len to maintain batch shape if actual_len < seq_len: pad_size = seq_len - actual_len # Pad seq dim only (last-but-one dim); hidden dim is untouched h = torch.nn.functional.pad(h, (0, 0, 0, pad_size)) - logits_padded = torch.nn.functional.pad( - outputs.logits, (0, 0, 0, pad_size) - ) + logits_padded = torch.nn.functional.pad(outputs.logits, (0, 0, 0, pad_size)) else: logits_padded = outputs.logits diff --git a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py index 90d8712e..ffe55eae 100755 --- a/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py +++ b/angelslim/compressor/speculative/train/trainer/online_dflash_trainer.py @@ -320,11 +320,11 @@ def step(self, closure=None): exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) - bias_correction1 = 1 - beta1 ** step_t - bias_correction2 = 1 - beta2 ** step_t + bias_correction1 = 1 - beta1**step_t + bias_correction2 = 1 - beta2**step_t step_size = lr / bias_correction1 - denom = (exp_avg_sq.sqrt() / (bias_correction2 ** 0.5)).add_(eps) + denom = (exp_avg_sq.sqrt() / (bias_correction2**0.5)).add_(eps) master_param.addcdiv_(exp_avg, denom, value=-step_size) @@ -365,8 +365,7 @@ def __init__( # Build fp32 master copies and replace the optimizer's param groups. self._fp32_params: List[torch.Tensor] = [ - p.detach().clone().to(torch.float32).requires_grad_(True) - for p in bf16_params + p.detach().clone().to(torch.float32).requires_grad_(True) for p in bf16_params ] assert len(inner_optimizer.param_groups) == 1, ( "_FP32MasterWeightOptimizer expects a single param group; " @@ -500,9 +499,7 @@ def __init__( self._gamma_init = self.loss_decay_gamma self.gamma_warmup = getattr(draft_model_config, "gamma_warmup", False) self._gamma_step = getattr(draft_model_config, "gamma_warmup_step", 0.5) - self.attention_backend = getattr( - draft_model_config, "attention_backend", "flex_attention" - ) + self.attention_backend = getattr(draft_model_config, "attention_backend", "flex_attention") self.mask_token_id = dflash_config.get( "mask_token_id", getattr(draft_model_config, "mask_token_id", None), @@ -567,9 +564,7 @@ def create_optimizer(self, model=None): if self.is_fsdp_enabled: args = self.args - param_groups = [ - {"params": [p for p in self.model.parameters() if p.requires_grad]} - ] + param_groups = [{"params": [p for p in self.model.parameters() if p.requires_grad]}] optimizer = _FP32StateAdamW( param_groups, lr=args.learning_rate, @@ -584,9 +579,7 @@ def create_optimizer(self, model=None): self.optimizer = optimizer return self.optimizer - bf16_params: List[torch.Tensor] = [ - p for p in self.model.parameters() if p.requires_grad - ] + bf16_params: List[torch.Tensor] = [p for p in self.model.parameters() if p.requires_grad] if not bf16_params: return super().create_optimizer(model) @@ -711,9 +704,7 @@ def _update_gamma_warmup(self): if not self.gamma_warmup or self._gamma_init is None: return current_epoch = int(self.state.epoch) if hasattr(self.state, "epoch") else 0 - self.loss_decay_gamma = self._gamma_init + self._gamma_step * float( - current_epoch - ) + self.loss_decay_gamma = self._gamma_init + self._gamma_step * float(current_epoch) def prepare_data_for_draft_model(self, inputs): """Prepare data for DFlash training. diff --git a/tools/train_dflash_offline.py b/tools/train_dflash_offline.py index d2dd32d2..f75914c5 100755 --- a/tools/train_dflash_offline.py +++ b/tools/train_dflash_offline.py @@ -176,7 +176,7 @@ def parse_args(): type=str, default="", help="FSDP configuration string passed to TrainingArguments " - "(e.g. 'shard_grad_op auto_wrap'). Empty disables FSDP.", + "(e.g. 'shard_grad_op auto_wrap'). Empty disables FSDP.", ) t.add_argument( "--fsdp_config", diff --git a/tools/train_dflash_online.py b/tools/train_dflash_online.py index 097495ae..1d2648a8 100755 --- a/tools/train_dflash_online.py +++ b/tools/train_dflash_online.py @@ -282,7 +282,7 @@ def parse_args(): type=str, default="", help="FSDP configuration string passed to TrainingArguments " - "(e.g. 'shard_grad_op auto_wrap'). Empty disables FSDP.", + "(e.g. 'shard_grad_op auto_wrap'). Empty disables FSDP.", ) training_group.add_argument( "--fsdp_config", From 47406ab38c88edef64034219d5c7140ad65b72fa Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 1 Jun 2026 16:16:56 +0800 Subject: [PATCH 19/23] dflare --- docs/source/assets/dflare/intro.png | Bin 0 -> 223551 bytes docs/source/assets/dflare/speedup.png | Bin 0 -> 91916 bytes .../features/speculative_decoding/dflare.md | 116 +++++ .../features/speculative_decoding/index.md | 1 + tools/dflash_benchmark.py | 480 ++++++++++++++++++ 5 files changed, 597 insertions(+) create mode 100644 docs/source/assets/dflare/intro.png create mode 100644 docs/source/assets/dflare/speedup.png create mode 100644 docs/source/features/speculative_decoding/dflare.md create mode 100644 tools/dflash_benchmark.py diff --git a/docs/source/assets/dflare/intro.png b/docs/source/assets/dflare/intro.png new file mode 100644 index 0000000000000000000000000000000000000000..38d38f11518e28cbadc9562a68dda330f329257c GIT binary patch literal 223551 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/source/assets/dflare/speedup.png b/docs/source/assets/dflare/speedup.png new file mode 100644 index 0000000000000000000000000000000000000000..860cb7f12a29f7e1cc987472c09dc2269fa4093f GIT binary patch literal 91916 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
=ty#hlb-%9h$kUc`VfpHcM7&SUNW_@ZMOt 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 B{-6K= literal 0 HcmV?d00001 diff --git a/docs/source/features/speculative_decoding/dflare.md b/docs/source/features/speculative_decoding/dflare.md new file mode 100644 index 00000000..596c2e62 --- /dev/null +++ b/docs/source/features/speculative_decoding/dflare.md @@ -0,0 +1,116 @@ +# DFlare + +**DFlare** is a block-diffusion speculative decoding framework that accelerates large language model inference by predicting an entire block of tokens in one shot for the target model to verify in parallel. It removes the narrow conditioning bottleneck of the prior state-of-the-art DFlash through a lightweight **layer-wise fusion** mechanism: each draft layer attends to its own learnable combination of a broad set of target layers at negligible overhead, simultaneously injecting richer target knowledge and giving every draft layer a distinct input. Combined with training-data scaling, this enhanced per-layer expressiveness allows the draft model to scale to deeper architectures with consistent gains, achieving up to **5.52× end-to-end speedup** without compromising output quality. + +This repository contains the official implementation and resources for the paper: **DFLARE: Scaling Up Draft Capacity for Block Diffusion Speculative Decoding**. + + +:::{image} /assets/dflare/intro.png +:alt: An overview of the DFlare framework. +::: + +--- + +## 🚀 Abstract + +Block diffusion speculative decoding accelerates LLM inference by predicting all tokens within a block simultaneously for the target model to verify in parallel. Predicting an entire block at once requires a sufficiently capable draft model and effective utilization of the target model's internal knowledge. However, the state-of-the-art method DFlash constrains all draft layers to share a single fused representation derived from only a few target layers, limiting per-layer expressiveness and hindering further scaling of draft capacity. We present **DFLARE**, which flares out the narrow conditioning bottleneck of DFlash through a lightweight layer-wise fusion mechanism: each draft layer attends to its own learnable combination of a broad set of target layers at negligible overhead, simultaneously injecting richer target knowledge and providing every draft layer with a distinct input. This enhanced per-layer expressiveness enables scaling the draft model to deeper architectures with consistent gains. We further scale training data from 800K to 2.4M samples to fully exploit the enlarged capacity. On six benchmarks spanning mathematical reasoning, code generation, and conversation, DFLARE attains average wall-clock speedups of **5.52× on Qwen3-4B**, **5.46× on Qwen3-8B**, and **3.91× on GPT-OSS-20B**, improving over DFlash by roughly 11%, 8%, and 5% respectively. + + +## ✨ Key Highlights + +- **Layer-wise Fusion for Richer Conditioning**: Replaces DFlash's single fused representation with a lightweight mechanism in which each draft layer attends to its own learnable combination of a broad set of target layers, removing the conditioning bottleneck at negligible overhead. +- **Scalable Draft Capacity**: The enriched per-layer expressiveness lets the draft model scale to deeper architectures with consistent gains, complemented by scaling training data from 800K to 2.4M samples to fully exploit the enlarged capacity. +- **Substantial End-to-End Speedups**: Across six benchmarks covering mathematical reasoning, code generation, and conversation, DFlare delivers average wall-clock speedups of 5.52× on Qwen3-4B, 5.46× on Qwen3-8B, and 3.91× on GPT-OSS-20B — roughly 11%, 8%, and 5% over DFlash respectively. + + +## ⚡ Quick Start + +### Training + +DFlare reuses the DFlash training pipeline and selects the layer-wise fusion architecture via `--draft_arch dflare`. Two entry points are provided: + +**Online training** (recommended) — runs the target model on the fly to produce hidden states each step. No data pre-generation step needed. + +```shell +export TARGET_MODEL_PATH=/path/to/Qwen3-4B +export TRAIN_DATA_PATH=/path/to/train.jsonl +export OUTPUT_DIR=/path/to/output + +bash scripts/speculative/run_dflare_online.sh 8 flex_attention +``` + +**Offline training** — trains from pre-computed hidden-state `.ckpt` files. First generate the cache with `scripts/speculative/generate_dflash_data.sh` using a DFlare-compatible draft config, then: + +```shell +export TARGET_MODEL_PATH=/path/to/Qwen3-4B +export TRAIN_HIDDEN_PATH=/path/to/hidden_cache +export OUTPUT_DIR=/path/to/output + +bash scripts/speculative/run_dflare_offline.sh 8 flex_attention +``` + +Both entries use the same defaults: `block_size=16`, `num_anchors=512`, `lr=6e-4`, cosine schedule with 4% warmup, `max_length=3072`, FSDP `shard_grad_op` with FP32 master-weights optimizer, and `flash_attention_2` for the target model. The default draft model config is `configs/qwen3_dflare.json`. + +### Inference and Evaluation + +To benchmark a trained DFlare draft model on tasks such as GSM8K, MT-Bench, MATH-500, and HumanEval, use `tools/dflash_benchmark.py`. The script supports both DFlash and DFlare draft architectures via the `--draft-arch` flag — for DFlare set `--draft-arch dflare`. It loads the matching `QwenDFlareDraftModel` class, runs block-parallel speculative decoding (block-size proposal from the draft + parallel target verification + longest-prefix accept), and reports decoding speedup, average acceptance length, and the per-block acceptance-length histogram. + +**Single-GPU evaluation:** + +```shell +python tools/dflash_benchmark.py \ + --model-name-or-path /path/to/Qwen3-4B \ + --draft-name-or-path /path/to/dflare_checkpoint \ + --draft-arch dflare \ + --dataset gsm8k \ + --max-samples 128 \ + --max-new-tokens 2048 \ + --temperature 0.0 \ + --block-size 16 +``` + +**Multi-GPU evaluation** (workload is sharded across ranks; results are gathered to rank 0): + +```shell +torchrun --nproc_per_node=8 --master_port=29600 \ + tools/dflash_benchmark.py \ + --model-name-or-path /path/to/Qwen3-4B \ + --draft-name-or-path /path/to/dflare_checkpoint \ + --draft-arch dflare \ + --dataset gsm8k \ + --max-samples 128 \ + --max-new-tokens 2048 \ + --temperature 0.0 \ + --block-size 16 +``` + +Notes: + +- `--block-size` is optional; if omitted, the script reads `block_size` directly from the loaded draft checkpoint's config. +- The script runs each prompt twice — once with `block_size=1` (vanilla AR decoding) and once with the speculative `block_size` — so the reported `Decoding speedup` is a self-contained ratio. No external baseline run is required. +- Both target and draft are loaded in `bfloat16` with `flash_attention_2` when `flash-attn` is installed (otherwise it falls back to PyTorch SDPA, which reduces wall-clock speedup but does not affect acceptance length). +- Supported datasets out of the box: `gsm8k`, `math500`, `aime24`, `aime25`, `alpaca`, `mt-bench`, `humaneval`, `mbpp`, `lbpp`, `swe-bench`, `livecodebench`. +- To compare DFlash and DFlare on the same checkpoint format, switch `--draft-arch dflash` and point `--draft-name-or-path` to a DFlash checkpoint — the rest of the command stays identical. + + +## 📈 Results + +We evaluate DFlare on six benchmarks spanning mathematical reasoning (GSM8K, MATH-500, AIME), code generation (HumanEval, MBPP, LiveCodeBench), and open-domain conversation (MT-Bench, Alpaca), against DFlash and EAGLE-3 baselines on Qwen3-4B, Qwen3-8B, and GPT-OSS-20B target models. + +:::{image} /assets/dflare/speedup.png +:alt: DFlare end-to-end speedup vs DFlash and EAGLE-3 across six benchmarks. +::: + + +## 📜 Citation + +If you find our work useful in your research, please consider citing our paper: + +```bibtex +@article{DFlare2026, + title={DFlare: Scaling Up Draft Capacity for Block Diffusion Speculative Decoding}, + author={Jiebin Zhang and Zhenghan Yu and Song Liu and Eugene J. Yu and Zheng Li and Dawei Zhu and Jiangshan Duo and Weimin Xiong and Yifan Song and Guanghua Yu and Jianchen Zhu and Sujian Li}, + journal={arXiv preprint arXiv}, + year={2026} +} +``` \ No newline at end of file diff --git a/docs/source/features/speculative_decoding/index.md b/docs/source/features/speculative_decoding/index.md index b8b8b641..263d4631 100644 --- a/docs/source/features/speculative_decoding/index.md +++ b/docs/source/features/speculative_decoding/index.md @@ -9,4 +9,5 @@ eagle/index spec_exit dcut +dflare ::: diff --git a/tools/dflash_benchmark.py b/tools/dflash_benchmark.py new file mode 100644 index 00000000..90fe45f0 --- /dev/null +++ b/tools/dflash_benchmark.py @@ -0,0 +1,480 @@ +"""DFlash / DFlare end-to-end speculative decoding benchmark. + +A self-contained evaluation entry point for AngelSlim's draft model classes. +Selects the draft architecture via ``--draft-arch``: + + --draft-arch dflash -> angelslim.compressor.speculative.train.models.draft + .qwen_dflash.QwenDFlashDraftModel + --draft-arch dflare -> angelslim.compressor.speculative.train.models.draft + .qwen_dflare.QwenDFlareDraftModel + +Reports decoding speedup vs single-token decoding and per-block acceptance +length distribution. Supports torchrun for multi-GPU sharded evaluation. + +Usage (single GPU):: + + python tools/dflash_benchmark.py \\ + --model-name-or-path /path/to/Qwen3-4B \\ + --draft-name-or-path /path/to/dflash_or_dflare_ckpt \\ + --draft-arch dflare \\ + --dataset gsm8k --max-samples 128 + +Usage (8 GPUs):: + + torchrun --nproc_per_node=8 --master_port=29600 \\ + tools/dflash_benchmark.py \\ + --model-name-or-path /path/to/Qwen3-4B \\ + --draft-name-or-path /path/to/dflare_ckpt \\ + --draft-arch dflare \\ + --dataset gsm8k --max-samples 128 +""" + +from __future__ import annotations + +import argparse +import os +import random +import time +import warnings +from itertools import chain +from types import SimpleNamespace +from typing import Any, List, Optional + +import numpy as np +import torch +from datasets import Features, Sequence, Value, load_dataset +from loguru import logger +from rich import print +from torch import distributed as torch_dist +from tqdm import tqdm +from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache + + +# --------------------------------------------------------------------------- +# Distributed helpers (small wrapper over torch.distributed; no extra package +# dependency on AngelSlim's side). +# --------------------------------------------------------------------------- +def _dist_init() -> None: + if "RANK" not in os.environ: + warnings.warn("Environment variable `RANK` is not set; running single-process.") + return + torch_dist.init_process_group(backend="nccl", init_method="env://") + + +def _dist_is_initialized() -> bool: + return torch_dist.is_initialized() + + +def _dist_size() -> int: + return int(os.environ.get("WORLD_SIZE", 1)) + + +def _dist_rank() -> int: + return int(os.environ.get("RANK", 0)) + + +def _dist_local_rank() -> int: + return int(os.environ.get("LOCAL_RANK", 0)) + + +def _dist_is_main() -> bool: + return _dist_rank() == 0 + + +def _dist_gather(obj: Any, dst: int = 0) -> Optional[List[Any]]: + if not _dist_is_initialized(): + return [obj] + if _dist_is_main(): + objs: List[Any] = [None for _ in range(_dist_size())] + torch_dist.gather_object(obj, objs, dst=dst) + return objs + torch_dist.gather_object(obj, dst=dst) + return None + + +# --------------------------------------------------------------------------- +# Dataset loader. Each loaded item must expose a ``turns`` field that is a +# list of user messages (one entry per turn for multi-turn datasets like +# mt-bench). +# --------------------------------------------------------------------------- +def load_and_process_dataset(data_name: str): + if data_name == "gsm8k": + ds = load_dataset("openai/gsm8k", "main", split="test") + fmt = "{question}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + return ds.map(lambda x: {"turns": [fmt.format(**x)]}) + + if data_name == "math500": + ds = load_dataset("HuggingFaceH4/MATH-500", split="test") + fmt = "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + return ds.map(lambda x: {"turns": [fmt.format(**x)]}) + + if data_name == "aime24": + ds = load_dataset("HuggingFaceH4/aime_2024", split="train") + fmt = "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + return ds.map(lambda x: {"turns": [fmt.format(**x)]}) + + if data_name == "aime25": + ds = load_dataset("MathArena/aime_2025", split="train") + fmt = "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + return ds.map(lambda x: {"turns": [fmt.format(**x)]}) + + if data_name == "alpaca": + ds = load_dataset("tatsu-lab/alpaca", split="train") + ds = ds.map( + lambda x: { + "formatted_input": ( + f"{x['instruction']}\n\nInput:\n{x['input']}" if x["input"] else x["instruction"] + ) + } + ) + return ds.map(lambda x: {"turns": [x["formatted_input"]]}) + + if data_name == "mt-bench": + ds = load_dataset("HuggingFaceH4/mt_bench_prompts", split="train") + return ds.map(lambda x: {"turns": x["prompt"]}) + + if data_name == "humaneval": + ds = load_dataset("openai/openai_humaneval", split="test") + fmt = ( + "Write a solution to the following problem and make sure that it passes the tests:\n" + "```python\n{prompt}\n```" + ) + return ds.map(lambda x: {"turns": [fmt.format(**x)]}) + + if data_name == "mbpp": + ds = load_dataset("google-research-datasets/mbpp", "sanitized", split="test") + return ds.map(lambda x: {"turns": [x["prompt"]]}) + + if data_name == "lbpp": + url = "https://huggingface.co/datasets/CohereLabs/lbpp/resolve/main/python/test.parquet" + ds = load_dataset("parquet", data_files={"test": url})["test"] + return ds.map(lambda x: {"turns": [x["instruction"]]}) + + if data_name == "swe-bench": + ds = load_dataset("princeton-nlp/SWE-bench_Lite", split="test") + fmt = "Problem Statement:\n{problem_statement}\nPlease fix the issue described above." + return ds.map(lambda x: {"turns": [fmt.format(**x)]}) + + if data_name == "livecodebench": + base = "https://huggingface.co/datasets/livecodebench/code_generation_lite/resolve/main/" + files = ["test.jsonl", "test2.jsonl", "test3.jsonl", "test4.jsonl", "test5.jsonl", "test6.jsonl"] + ds = load_dataset("json", data_files={"test": [base + fn for fn in files]})["test"] + + def _fmt(doc): + sys = ( + "You are an expert Python programmer. You will be given a question (problem specification) " + "and will generate a correct Python program that matches the specification and passes all tests. " + "You will NOT return anything except for the program" + ) + q = f"### Question:\n{doc['question_content']}" + if doc.get("starter_code"): + fmt_msg = "### Format: Use the following code structure:" + code = f"```python\n{doc['starter_code']}\n```" + else: + fmt_msg = "### Format: Write your code in the following format:" + code = "```python\n# YOUR CODE HERE\n```" + tail = "### Answer: (use the provided format with backticks)" + return f"{sys}\n\n{q}\n\n{fmt_msg}\n{code}\n\n{tail}" + + target_features = Features({"turns": Sequence(Value("large_string"))}) + return ds.map( + lambda x: {"turns": [_fmt(x)]}, + remove_columns=ds.column_names, + features=target_features, + ) + + raise ValueError(f"Unknown dataset: {data_name}") + + +# --------------------------------------------------------------------------- +# Draft architecture dispatch. +# --------------------------------------------------------------------------- +def _resolve_draft_arch(arch: str): + """Return (DraftModelClass, sample_fn, extract_context_feature_fn).""" + arch = arch.lower() + if arch == "dflash": + from angelslim.compressor.speculative.train.models.draft.qwen_dflash import ( + QwenDFlashDraftModel, + extract_context_feature, + sample, + ) + return QwenDFlashDraftModel, sample, extract_context_feature + if arch == "dflare": + from angelslim.compressor.speculative.train.models.draft.qwen_dflare import ( + QwenDFlareDraftModel, + extract_context_feature, + sample, + ) + return QwenDFlareDraftModel, sample, extract_context_feature + raise ValueError(f"--draft-arch must be one of {{dflash, dflare}}, got: {arch}") + + +# --------------------------------------------------------------------------- +# Speculative-decoding loop: block-parallel draft proposal, target +# verification, longest-prefix accept. +# --------------------------------------------------------------------------- +def cuda_time() -> float: + torch.cuda.synchronize() + return time.perf_counter() + + +@torch.inference_mode() +def dflash_generate( + model, + target, + input_ids: torch.Tensor, + mask_token_id: int, + max_new_tokens: int, + block_size: int, + stop_token_ids: list, + sample_fn, + extract_context_feature_fn, + temperature: float = 0.0, +) -> SimpleNamespace: + num_input_tokens = input_ids.shape[1] + max_length = num_input_tokens + max_new_tokens + + output_ids = torch.full( + (1, max_length + block_size), + mask_token_id, + dtype=torch.long, + device=model.device, + ) + position_ids = torch.arange(output_ids.shape[1], device=model.device).unsqueeze(0) + past_key_values_target = DynamicCache() + past_key_values_draft = DynamicCache() + + # Prefill stage + prefill_start = cuda_time() + output = target( + input_ids, + position_ids=position_ids[:, :num_input_tokens], + past_key_values=past_key_values_target, + use_cache=True, + logits_to_keep=1, + output_hidden_states=True if block_size > 1 else False, + ) + + output_ids[:, :num_input_tokens] = input_ids + output_ids[:, num_input_tokens : num_input_tokens + 1] = sample_fn(output.logits, temperature) + if block_size > 1: + target_hidden = extract_context_feature_fn(output.hidden_states, model.target_layer_ids) + + time_to_first_token = cuda_time() - prefill_start + + # Decode stage + decode_start = cuda_time() + start = input_ids.shape[1] + acceptance_lengths = [] + draft_prefill = True + + while start < max_length: + block_output_ids = output_ids[:, start : start + block_size].clone() + block_position_ids = position_ids[:, start : start + block_size] + if block_size > 1: + noise_embedding = target.model.embed_tokens(block_output_ids) + draft_logits = target.lm_head( + model( + target_hidden=target_hidden, + noise_embedding=noise_embedding, + position_ids=position_ids[ + :, past_key_values_draft.get_seq_length() : start + block_size + ], + past_key_values=past_key_values_draft, + use_cache=True, + is_causal=False, + )[:, -block_size + 1 :, :] + ) + past_key_values_draft.crop(start) + block_output_ids[:, 1:] = sample_fn(draft_logits) + if draft_prefill: + draft_prefill = False + decode_start = cuda_time() + + output = target( + block_output_ids, + position_ids=block_position_ids, + past_key_values=past_key_values_target, + use_cache=True, + output_hidden_states=True if block_size > 1 else False, + ) + + posterior = sample_fn(output.logits, temperature) + acceptance_length = ( + (block_output_ids[:, 1:] == posterior[:, :-1]).cumprod(dim=1).sum(dim=1)[0].item() + ) + output_ids[:, start : start + acceptance_length + 1] = block_output_ids[ + :, : acceptance_length + 1 + ] + output_ids[:, start + acceptance_length + 1] = posterior[:, acceptance_length] + + acceptance_lengths.append(acceptance_length + 1) + start += acceptance_length + 1 + past_key_values_target.crop(start) + if block_size > 1: + target_hidden = extract_context_feature_fn(output.hidden_states, model.target_layer_ids)[ + :, : acceptance_length + 1, : + ] + + if stop_token_ids is not None and any( + stop_token_id in output_ids[:, num_input_tokens:] for stop_token_id in stop_token_ids + ): + break + + output_ids = output_ids[:, :max_length] + output_ids = output_ids[:, output_ids[0] != mask_token_id] + if stop_token_ids is not None: + stop_tensor = torch.tensor(stop_token_ids, device=output_ids.device) + stop_indices = torch.isin(output_ids[0][num_input_tokens:], stop_tensor).nonzero(as_tuple=True)[0] + if stop_indices.numel() > 0: + output_ids = output_ids[:, : num_input_tokens + stop_indices[0] + 1] + + num_output_tokens = output_ids.shape[1] - num_input_tokens + total_decode_time = cuda_time() - decode_start + time_per_output_token = total_decode_time / num_output_tokens + + return SimpleNamespace( + output_ids=output_ids, + num_input_tokens=num_input_tokens, + num_output_tokens=num_output_tokens, + time_to_first_token=time_to_first_token, + time_per_output_token=time_per_output_token, + acceptance_lengths=acceptance_lengths, + ) + + +# --------------------------------------------------------------------------- +# Entry point. +# --------------------------------------------------------------------------- +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--model-name-or-path", type=str, required=True, + help="Path or HF id of the target model.") + parser.add_argument("--draft-name-or-path", type=str, required=True, + help="Path of the trained DFlash/DFlare draft checkpoint.") + parser.add_argument("--draft-arch", type=str, choices=["dflash", "dflare"], required=True, + help="Which AngelSlim draft architecture to load.") + parser.add_argument("--block-size", type=int, default=None, + help="Speculative block size. Defaults to draft model's config value.") + parser.add_argument("--dataset", type=str, required=True, + help="Dataset name; see load_and_process_dataset() for the supported list.") + parser.add_argument("--max-samples", type=int, default=None) + parser.add_argument("--max-new-tokens", type=int, default=16384) + parser.add_argument("--temperature", type=float, default=0.0) + args = parser.parse_args() + + random.seed(0) + np.random.seed(0) + torch.manual_seed(0) + torch.cuda.manual_seed_all(0) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + _dist_init() + torch.cuda.set_device(_dist_local_rank()) + device = torch.device(f"cuda:{_dist_local_rank()}") + + DraftModelCls, sample_fn, extract_context_feature_fn = _resolve_draft_arch(args.draft_arch) + + def has_flash_attn() -> bool: + try: + import flash_attn # noqa: F401 + return True + except ImportError: + logger.warning( + "flash_attn is not installed; falling back to torch.sdpa. " + "End-to-end speedup will be lower." + ) + return False + + installed_flash_attn = has_flash_attn() + attn_impl = "flash_attention_2" if installed_flash_attn else "sdpa" + + target = ( + AutoModelForCausalLM.from_pretrained( + args.model_name_or_path, + attn_implementation=attn_impl, + dtype=torch.bfloat16, + ) + .to(device) + .eval() + ) + + draft_model = ( + DraftModelCls.from_pretrained( + args.draft_name_or_path, + attn_implementation=attn_impl, + dtype=torch.bfloat16, + local_files_only=True, + ) + .to(device) + .eval() + ) + + block_size = args.block_size if args.block_size is not None else draft_model.block_size + + tokenizer = AutoTokenizer.from_pretrained(args.model_name_or_path) + dataset = load_and_process_dataset(args.dataset) + + if args.max_samples is not None and len(dataset) > args.max_samples: + dataset = dataset.shuffle(seed=0).select(range(args.max_samples)) + + responses = [] + indices = range(_dist_rank(), len(dataset), _dist_size()) + for idx in tqdm(indices, disable=not _dist_is_main()): + instance = dataset[idx] + messages = [] + for user_content in instance["turns"]: + messages.append({"role": "user", "content": user_content}) + input_text = tokenizer.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True, enable_thinking=False + ) + input_ids = tokenizer.encode(input_text, return_tensors="pt").to(target.device) + + response = {} + for bs in [1, block_size]: + response[bs] = dflash_generate( + model=draft_model, + target=target, + input_ids=input_ids, + mask_token_id=draft_model.mask_token_id, + max_new_tokens=args.max_new_tokens, + block_size=bs, + stop_token_ids=[tokenizer.eos_token_id], + sample_fn=sample_fn, + extract_context_feature_fn=extract_context_feature_fn, + temperature=args.temperature, + ) + + spec_response = response[block_size] + generated_ids = spec_response.output_ids[0, spec_response.num_input_tokens :] + output_text = tokenizer.decode(generated_ids, skip_special_tokens=True) + messages.append({"role": "assistant", "content": output_text}) + responses.append(response) + + if _dist_size() > 1: + gathered = _dist_gather(responses, dst=0) + if not _dist_is_main(): + return + responses = list(chain(*gathered)) + + if not responses: + return + + t1 = np.mean([r[1].time_per_output_token for r in responses]) + tb = np.mean([r[block_size].time_per_output_token for r in responses]) + print(f"[draft_arch={args.draft_arch}] Decoding speedup: {t1 / tb:.2f}") + + tau = np.mean([np.mean(r[block_size].acceptance_lengths) for r in responses]) + print(f"[draft_arch={args.draft_arch}] Average Acceptance length: {tau:.2f}") + + acceptance_lengths = list(chain(*[r[block_size].acceptance_lengths for r in responses])) + histogram = [acceptance_lengths.count(b) / len(acceptance_lengths) for b in range(block_size + 1)] + print( + f"[draft_arch={args.draft_arch}] Acceptance length histogram: " + f"{[f'{x * 100:.1f}%' for x in histogram]}" + ) + + +if __name__ == "__main__": + main() From cb44f49402635e1db278ffd29d81c8507c08bd3f Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 1 Jun 2026 16:26:21 +0800 Subject: [PATCH 20/23] dflare --- tools/dflash_benchmark.py | 94 +++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/tools/dflash_benchmark.py b/tools/dflash_benchmark.py index 90fe45f0..376311d1 100644 --- a/tools/dflash_benchmark.py +++ b/tools/dflash_benchmark.py @@ -56,7 +56,10 @@ # --------------------------------------------------------------------------- def _dist_init() -> None: if "RANK" not in os.environ: - warnings.warn("Environment variable `RANK` is not set; running single-process.") + warnings.warn( + "Environment variable `RANK` is not set; running single-process.", + stacklevel=2, + ) return torch_dist.init_process_group(backend="nccl", init_method="env://") @@ -100,22 +103,30 @@ def _dist_gather(obj: Any, dst: int = 0) -> Optional[List[Any]]: def load_and_process_dataset(data_name: str): if data_name == "gsm8k": ds = load_dataset("openai/gsm8k", "main", split="test") - fmt = "{question}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + fmt = ( + "{question}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + ) return ds.map(lambda x: {"turns": [fmt.format(**x)]}) if data_name == "math500": ds = load_dataset("HuggingFaceH4/MATH-500", split="test") - fmt = "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + fmt = ( + "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + ) return ds.map(lambda x: {"turns": [fmt.format(**x)]}) if data_name == "aime24": ds = load_dataset("HuggingFaceH4/aime_2024", split="train") - fmt = "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + fmt = ( + "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + ) return ds.map(lambda x: {"turns": [fmt.format(**x)]}) if data_name == "aime25": ds = load_dataset("MathArena/aime_2025", split="train") - fmt = "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + fmt = ( + "{problem}\nPlease reason step by step, and put your final answer within \\boxed{{}}." + ) return ds.map(lambda x: {"turns": [fmt.format(**x)]}) if data_name == "alpaca": @@ -123,7 +134,9 @@ def load_and_process_dataset(data_name: str): ds = ds.map( lambda x: { "formatted_input": ( - f"{x['instruction']}\n\nInput:\n{x['input']}" if x["input"] else x["instruction"] + f"{x['instruction']}\n\nInput:\n{x['input']}" + if x["input"] + else x["instruction"] ) } ) @@ -157,13 +170,21 @@ def load_and_process_dataset(data_name: str): if data_name == "livecodebench": base = "https://huggingface.co/datasets/livecodebench/code_generation_lite/resolve/main/" - files = ["test.jsonl", "test2.jsonl", "test3.jsonl", "test4.jsonl", "test5.jsonl", "test6.jsonl"] + files = [ + "test.jsonl", + "test2.jsonl", + "test3.jsonl", + "test4.jsonl", + "test5.jsonl", + "test6.jsonl", + ] ds = load_dataset("json", data_files={"test": [base + fn for fn in files]})["test"] def _fmt(doc): sys = ( - "You are an expert Python programmer. You will be given a question (problem specification) " - "and will generate a correct Python program that matches the specification and passes all tests. " + "You are an expert Python programmer. You will be given a question " + "(problem specification) and will generate a correct Python program " + "that matches the specification and passes all tests. " "You will NOT return anything except for the program" ) q = f"### Question:\n{doc['question_content']}" @@ -198,6 +219,7 @@ def _resolve_draft_arch(arch: str): extract_context_feature, sample, ) + return QwenDFlashDraftModel, sample, extract_context_feature if arch == "dflare": from angelslim.compressor.speculative.train.models.draft.qwen_dflare import ( @@ -205,6 +227,7 @@ def _resolve_draft_arch(arch: str): extract_context_feature, sample, ) + return QwenDFlareDraftModel, sample, extract_context_feature raise ValueError(f"--draft-arch must be one of {{dflash, dflare}}, got: {arch}") @@ -312,9 +335,9 @@ def dflash_generate( start += acceptance_length + 1 past_key_values_target.crop(start) if block_size > 1: - target_hidden = extract_context_feature_fn(output.hidden_states, model.target_layer_ids)[ - :, : acceptance_length + 1, : - ] + target_hidden = extract_context_feature_fn( + output.hidden_states, model.target_layer_ids + )[:, : acceptance_length + 1, :] if stop_token_ids is not None and any( stop_token_id in output_ids[:, num_input_tokens:] for stop_token_id in stop_token_ids @@ -325,7 +348,9 @@ def dflash_generate( output_ids = output_ids[:, output_ids[0] != mask_token_id] if stop_token_ids is not None: stop_tensor = torch.tensor(stop_token_ids, device=output_ids.device) - stop_indices = torch.isin(output_ids[0][num_input_tokens:], stop_tensor).nonzero(as_tuple=True)[0] + stop_indices = torch.isin(output_ids[0][num_input_tokens:], stop_tensor).nonzero( + as_tuple=True + )[0] if stop_indices.numel() > 0: output_ids = output_ids[:, : num_input_tokens + stop_indices[0] + 1] @@ -348,16 +373,34 @@ def dflash_generate( # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("--model-name-or-path", type=str, required=True, - help="Path or HF id of the target model.") - parser.add_argument("--draft-name-or-path", type=str, required=True, - help="Path of the trained DFlash/DFlare draft checkpoint.") - parser.add_argument("--draft-arch", type=str, choices=["dflash", "dflare"], required=True, - help="Which AngelSlim draft architecture to load.") - parser.add_argument("--block-size", type=int, default=None, - help="Speculative block size. Defaults to draft model's config value.") - parser.add_argument("--dataset", type=str, required=True, - help="Dataset name; see load_and_process_dataset() for the supported list.") + parser.add_argument( + "--model-name-or-path", type=str, required=True, help="Path or HF id of the target model." + ) + parser.add_argument( + "--draft-name-or-path", + type=str, + required=True, + help="Path of the trained DFlash/DFlare draft checkpoint.", + ) + parser.add_argument( + "--draft-arch", + type=str, + choices=["dflash", "dflare"], + required=True, + help="Which AngelSlim draft architecture to load.", + ) + parser.add_argument( + "--block-size", + type=int, + default=None, + help="Speculative block size. Defaults to draft model's config value.", + ) + parser.add_argument( + "--dataset", + type=str, + required=True, + help="Dataset name; see load_and_process_dataset() for the supported list.", + ) parser.add_argument("--max-samples", type=int, default=None) parser.add_argument("--max-new-tokens", type=int, default=16384) parser.add_argument("--temperature", type=float, default=0.0) @@ -379,6 +422,7 @@ def main() -> None: def has_flash_attn() -> bool: try: import flash_attn # noqa: F401 + return True except ImportError: logger.warning( @@ -469,7 +513,9 @@ def has_flash_attn() -> bool: print(f"[draft_arch={args.draft_arch}] Average Acceptance length: {tau:.2f}") acceptance_lengths = list(chain(*[r[block_size].acceptance_lengths for r in responses])) - histogram = [acceptance_lengths.count(b) / len(acceptance_lengths) for b in range(block_size + 1)] + histogram = [ + acceptance_lengths.count(b) / len(acceptance_lengths) for b in range(block_size + 1) + ] print( f"[draft_arch={args.draft_arch}] Acceptance length histogram: " f"{[f'{x * 100:.1f}%' for x in histogram]}" From a94e8c3d6227b31009f52eb1ef74a4ad16120607 Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 1 Jun 2026 16:30:33 +0800 Subject: [PATCH 21/23] dflare --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4d0ac956..fd3e067c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,10 @@ eval/ *_ckpt*/ output/ outputs/ +output*/ +logs*/ outs/ wandb/ tools/results/ __pycache__/outputs/ +__pycache__/ From 67fc20b779b0a9c1fb502b02a2db2f0ee9d3c98d Mon Sep 17 00:00:00 2001 From: Jiebin Zhang <84370564+zhzihao@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:43:30 +0800 Subject: [PATCH 22/23] dflare From 7ce60fc4fd95237cd41712fd898a78a3657b303d Mon Sep 17 00:00:00 2001 From: jiebinzhang Date: Mon, 1 Jun 2026 16:54:23 +0800 Subject: [PATCH 23/23] dflare --- .gitignore | 2 +- README.md | 2 ++ README_cn.md | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fd3e067c..d8fcb3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,4 @@ outs/ wandb/ tools/results/ __pycache__/outputs/ -__pycache__/ +__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index de3c3f1e..7d952b65 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A more accessible, comprehensive, and efficient toolkit for large model compress

## 📣Latest News +- [26/06/01] We have released **DFlare**, a block-diffusion speculative decoding framework with layer-wise fusion that achieves up to **5.52× end-to-end speedup**. [[Docs]](https://angelslim.readthedocs.io/zh-cn/latest/features/speculative_decoding/dflare.html) - [26/05/27] We have released **D-Cut**, an adaptive verification depth pruning technique for speculative decoding. [[Docs]](https://angelslim.readthedocs.io/zh-cn/latest/dcut.html) - [26/05/20] We support Distillation for full-precision HuggingFace models and **quantized QAT-style** models, as detailed in the [distillation documentation](https://angelslim.readthedocs.io/zh-cn/latest/features/distill/index.html). - [26/05/08] We have released STQ1_0 kernel for 1.25-bit model and given a PR to llama.cpp [PR #22836](https://github.com/ggml-org/llama.cpp/pull/22836) ! If you have any questions or suggestions for STQ_0, welcome to comment under the PR !🔥🔥🔥 @@ -92,6 +93,7 @@ A more accessible, comprehensive, and efficient toolkit for large model compress
diff --git a/README_cn.md b/README_cn.md index 079ab305..ec72132c 100644 --- a/README_cn.md +++ b/README_cn.md @@ -22,6 +22,7 @@

## 📣最新进展 +- [26/06/01] 我们发布了 **DFlare**,一种基于 layer-wise fusion 的块扩散投机解码框架,端到端加速比可达 **5.52×**。[[文档]](https://angelslim.readthedocs.io/zh-cn/latest/features/speculative_decoding/dflare.html) - [26/05/27] 我们发布了 **D-Cut**,一种用于投机解码的自适应验证深度裁剪技术。[[文档]](https://angelslim.readthedocs.io/zh-cn/latest/dcut.html) - [26/05/20] 我们支持了模型蒸馏功能,适用于huggingface 全精度或者**QAT量化**模型,详细步骤可以参考[文档](https://angelslim.readthedocs.io/zh-cn/latest/features/distill/index.html).🔥🔥🔥 - [26/05/08] 我们发布了用于 1.25-bit 模型的 STQ1_0 内核,并向 llama.cpp 提交了 [PR #22836](https://github.com/ggml-org/llama.cpp/pull/22836)!如果您对 STQ_0 有任何疑问或建议,欢迎在该 PR 下留言!🔥🔥🔥 @@ -93,6 +94,7 @@