Spaces:
Running
v16: fix critical audit defects (N6/N7/N8/N9 + S3/S4/M2/M5)
Browse filesCRITICAL:
- N6: apply_rotary_pos_emb now slices cos/sin by position_ids, preventing
broadcast mismatch during incremental generation (Q shape (B,H,1,D) would
broadcast to (B,H,kv_seq_len,D) causing wrong output)
- N6: FusionAttention.forward() builds position_ids from KV cache offset
when not provided, ensuring correct RoPE at every step
SEVERE:
- N7: SBLA _cached_block_latents null safety - clear cache when use_cache=False,
validate batch_size matches to prevent cross-batch contamination
- S3: LoRA target_modules now includes SBLAttention latent projections
(latent_q/k/v_proj, latent_out_proj, v_to_hidden_proj)
- S4: Extract create_local_model() into shared train/model_utils.py,
both lora_finetune.py and full_finetune.py delegate to it
MEDIUM:
- N8: Remove dead depth_bias computation in ThinkingDialModel.forward()
- N9: apply_rotary_pos_emb accepts position_ids parameter (was unused)
- M2: THINK_END changed from fragile '|>' to proper '<|think_end|>' special
token; separated THINK_CLOSE for think_depth token closing bracket
- M5: generate_with_thinking() now passes thinking_depth to model.generate()
Tests: 12/12 passed
- models/fusion_model.py +22 -9
- models/sbla_attention.py +23 -18
- models/thinking_dial.py +9 -10
- train/full_finetune.py +9 -45
- train/lora_finetune.py +18 -65
- train/model_utils.py +76 -0
|
@@ -150,17 +150,23 @@ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None):
|
|
| 150 |
|
| 151 |
Args:
|
| 152 |
q: (batch, num_heads, seq_len, head_dim)
|
| 153 |
-
k: (batch,
|
| 154 |
-
cos: (
|
| 155 |
-
sin: (
|
| 156 |
-
position_ids:
|
| 157 |
|
| 158 |
Returns:
|
| 159 |
Tuple of (q_embed, k_embed) with rotary position encoding applied.
|
| 160 |
"""
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
q_embed = (q * cos) + (rotate_half(q) * sin)
|
| 165 |
k_embed = (k * cos) + (rotate_half(k) * sin)
|
| 166 |
return q_embed, k_embed
|
|
@@ -250,8 +256,15 @@ class FusionAttention(nn.Module):
|
|
| 250 |
emb = self.rotary_emb(kv_seq_len, device=hidden_states.device)
|
| 251 |
cos = emb.cos()
|
| 252 |
sin = emb.sin()
|
| 253 |
-
#
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
# Store RoPE'd K/V in SBLAttention's cache for incremental generation
|
| 257 |
# S1 FIXED: KV Cache now works natively through SBLAttention.
|
|
|
|
| 150 |
|
| 151 |
Args:
|
| 152 |
q: (batch, num_heads, seq_len, head_dim)
|
| 153 |
+
k: (batch, num_kv_heads, seq_len, head_dim)
|
| 154 |
+
cos: (kv_seq_len, head_dim) cosine part of rotary embedding
|
| 155 |
+
sin: (kv_seq_len, head_dim) sine part of rotary embedding
|
| 156 |
+
position_ids: (batch, seq_len) position ids for slicing cos/sin
|
| 157 |
|
| 158 |
Returns:
|
| 159 |
Tuple of (q_embed, k_embed) with rotary position encoding applied.
|
| 160 |
"""
|
| 161 |
+
if position_ids is not None:
|
| 162 |
+
# N6 FIX: Slice cos/sin by position_ids to match actual Q/K positions
|
| 163 |
+
# position_ids: (batch, seq_len), cos/sin: (kv_seq_len, head_dim)
|
| 164 |
+
cos = cos[position_ids].unsqueeze(1) # (batch, 1, seq_len, head_dim)
|
| 165 |
+
sin = sin[position_ids].unsqueeze(1) # (batch, 1, seq_len, head_dim)
|
| 166 |
+
else:
|
| 167 |
+
# Fallback: broadcast for full-sequence (prefill) when position_ids not provided
|
| 168 |
+
cos = cos.unsqueeze(0).unsqueeze(0) # (1, 1, kv_seq_len, head_dim)
|
| 169 |
+
sin = sin.unsqueeze(0).unsqueeze(0)
|
| 170 |
q_embed = (q * cos) + (rotate_half(q) * sin)
|
| 171 |
k_embed = (k * cos) + (rotate_half(k) * sin)
|
| 172 |
return q_embed, k_embed
|
|
|
|
| 256 |
emb = self.rotary_emb(kv_seq_len, device=hidden_states.device)
|
| 257 |
cos = emb.cos()
|
| 258 |
sin = emb.sin()
|
| 259 |
+
# N6 FIX: Build position_ids for proper RoPE slicing
|
| 260 |
+
if position_ids is None:
|
| 261 |
+
if past_key_value is not None:
|
| 262 |
+
offset = past_key_value[0].shape[2]
|
| 263 |
+
position_ids = torch.arange(offset, offset + seq_len, device=hidden_states.device).unsqueeze(0)
|
| 264 |
+
else:
|
| 265 |
+
position_ids = torch.arange(seq_len, device=hidden_states.device).unsqueeze(0)
|
| 266 |
+
# Apply RoPE with position_ids to prevent broadcast mismatch during incremental generation
|
| 267 |
+
Q, K = apply_rotary_pos_emb(Q, K, cos, sin, position_ids=position_ids)
|
| 268 |
|
| 269 |
# Store RoPE'd K/V in SBLAttention's cache for incremental generation
|
| 270 |
# S1 FIXED: KV Cache now works natively through SBLAttention.
|
|
@@ -393,24 +393,26 @@ class SBLAttention(nn.Module):
|
|
| 393 |
# Incremental step: use cached block latents if available
|
| 394 |
if hasattr(self, '_cached_block_latents') and self._cached_block_latents is not None:
|
| 395 |
cached_q, cached_k, cached_v, cached_num_blocks = self._cached_block_latents
|
| 396 |
-
#
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
blk_q_inc
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
|
|
|
|
|
|
| 414 |
else:
|
| 415 |
# No cached latents: fall back to standard attention only
|
| 416 |
output = output_std
|
|
@@ -464,6 +466,9 @@ class SBLAttention(nn.Module):
|
|
| 464 |
if use_cache and past_key_value is None:
|
| 465 |
# Prefill step: cache block latents for subsequent incremental steps
|
| 466 |
self._cached_block_latents = (blk_q, blk_k, blk_v, num_blocks)
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
return output, present_key_value
|
| 469 |
|
|
|
|
| 393 |
# Incremental step: use cached block latents if available
|
| 394 |
if hasattr(self, '_cached_block_latents') and self._cached_block_latents is not None:
|
| 395 |
cached_q, cached_k, cached_v, cached_num_blocks = self._cached_block_latents
|
| 396 |
+
# N7 FIX: Validate batch size matches to prevent cross-batch contamination
|
| 397 |
+
if cached_q.size(0) != batch_size:
|
| 398 |
+
# Batch size changed (e.g., different batch in concurrent usage)
|
| 399 |
+
output = output_std
|
| 400 |
+
else:
|
| 401 |
+
# Compute latent query for the single new token
|
| 402 |
+
V_current_expanded = self._repeat_kv(V_current, self.num_kv_groups)
|
| 403 |
+
V_reshaped_inc = V_current_expanded.transpose(1, 2).contiguous().view(batch_size, seq_len, -1)
|
| 404 |
+
hidden_approx_inc = self.v_to_hidden_proj(V_reshaped_inc)
|
| 405 |
+
blk_q_inc = self.latent_q_proj(hidden_approx_inc)
|
| 406 |
+
# Attend to cached block keys/values
|
| 407 |
+
latent_attn_scores = torch.matmul(
|
| 408 |
+
blk_q_inc, cached_k.transpose(-1, -2)
|
| 409 |
+
) / math.sqrt(self.latent_dim)
|
| 410 |
+
latent_attn_probs = F.softmax(latent_attn_scores, dim=-1)
|
| 411 |
+
latent_attn_probs = self.dropout(latent_attn_probs)
|
| 412 |
+
latent_context = torch.matmul(latent_attn_probs, cached_v)
|
| 413 |
+
latent_output = self.latent_out_proj(latent_context)
|
| 414 |
+
gate_value = torch.sigmoid(self.gate)
|
| 415 |
+
output = output_std + gate_value * latent_output
|
| 416 |
else:
|
| 417 |
# No cached latents: fall back to standard attention only
|
| 418 |
output = output_std
|
|
|
|
| 466 |
if use_cache and past_key_value is None:
|
| 467 |
# Prefill step: cache block latents for subsequent incremental steps
|
| 468 |
self._cached_block_latents = (blk_q, blk_k, blk_v, num_blocks)
|
| 469 |
+
elif past_key_value is None:
|
| 470 |
+
# N7 FIX: Ensure cache is cleared when not using cache, prevents stale data
|
| 471 |
+
self._cached_block_latents = None
|
| 472 |
|
| 473 |
return output, present_key_value
|
| 474 |
|
|
@@ -47,8 +47,9 @@ from transformers import PreTrainedModel, GenerationMixin
|
|
| 47 |
# ============================================================
|
| 48 |
|
| 49 |
THINK_START = "<|think_depth_"
|
| 50 |
-
|
| 51 |
-
|
|
|
|
| 52 |
|
| 53 |
# Depth 0-3 的描述
|
| 54 |
THINK_DEPTH_DESCRIPTIONS = {
|
|
@@ -72,7 +73,7 @@ def build_think_token(depth: int) -> str:
|
|
| 72 |
if not 0 <= depth <= 3:
|
| 73 |
raise ValueError(f"depth 必须在 0-3 之间,当前值:{depth}")
|
| 74 |
|
| 75 |
-
return f"{THINK_START}{depth}{
|
| 76 |
|
| 77 |
|
| 78 |
def parse_think_token(text: str) -> Optional[int]:
|
|
@@ -430,6 +431,8 @@ class GRPOTrainer:
|
|
| 430 |
返回:
|
| 431 |
生成的文本列表
|
| 432 |
"""
|
|
|
|
|
|
|
| 433 |
outputs = self.model.generate(
|
| 434 |
input_ids=input_ids,
|
| 435 |
max_new_tokens=max_new_tokens,
|
|
@@ -438,6 +441,7 @@ class GRPOTrainer:
|
|
| 438 |
do_sample=True,
|
| 439 |
pad_token_id=self.model.config.pad_token_id or 0,
|
| 440 |
eos_token_id=self.model.config.eos_token_id or 1,
|
|
|
|
| 441 |
**kwargs,
|
| 442 |
)
|
| 443 |
|
|
@@ -745,13 +749,8 @@ class ThinkingDialModel(nn.Module):
|
|
| 745 |
if thinking_depth is not None:
|
| 746 |
depth_idx = thinking_depth.long().clamp(0, self.thinking_config.num_thinking_depths - 1)
|
| 747 |
depth_embedding = self.thinking_embedding(depth_idx) # (batch, hidden_size)
|
| 748 |
-
#
|
| 749 |
-
|
| 750 |
-
depth_embedding.unsqueeze(1), # (batch, 1, hidden_size)
|
| 751 |
-
self.thinking_embedding.weight.t(), # (hidden_size, num_depths) -> transpose gives (num_depths, hidden_size)
|
| 752 |
-
).squeeze(1) # (batch, num_depths)
|
| 753 |
-
# To properly project to vocab space, use base model's lm_head
|
| 754 |
-
# Simpler: add hidden_size bias via learned projection
|
| 755 |
depth_hidden = self.thinking_gate * depth_embedding # (batch, hidden_size)
|
| 756 |
# Add as residual to logits via lm_head projection
|
| 757 |
if hasattr(self.base_model, 'lm_head'):
|
|
|
|
| 47 |
# ============================================================
|
| 48 |
|
| 49 |
THINK_START = "<|think_depth_"
|
| 50 |
+
THINK_CLOSE = "|>" # Closing bracket for think_depth token
|
| 51 |
+
THINK_END = "<|think_end|>" # End-of-thinking-block marker
|
| 52 |
+
THINK_DEPTH_PATTERN = re.compile(r"<\|think_depth_(\d)\|>")
|
| 53 |
|
| 54 |
# Depth 0-3 的描述
|
| 55 |
THINK_DEPTH_DESCRIPTIONS = {
|
|
|
|
| 73 |
if not 0 <= depth <= 3:
|
| 74 |
raise ValueError(f"depth 必须在 0-3 之间,当前值:{depth}")
|
| 75 |
|
| 76 |
+
return f"{THINK_START}{depth}{THINK_CLOSE}"
|
| 77 |
|
| 78 |
|
| 79 |
def parse_think_token(text: str) -> Optional[int]:
|
|
|
|
| 431 |
返回:
|
| 432 |
生成的文本列表
|
| 433 |
"""
|
| 434 |
+
# M5 FIX: Pass thinking_depth to model.generate() so the ThinkingDialModel
|
| 435 |
+
# forward() can actually apply depth-dependent bias to logits
|
| 436 |
outputs = self.model.generate(
|
| 437 |
input_ids=input_ids,
|
| 438 |
max_new_tokens=max_new_tokens,
|
|
|
|
| 441 |
do_sample=True,
|
| 442 |
pad_token_id=self.model.config.pad_token_id or 0,
|
| 443 |
eos_token_id=self.model.config.eos_token_id or 1,
|
| 444 |
+
thinking_depth=thinking_depth,
|
| 445 |
**kwargs,
|
| 446 |
)
|
| 447 |
|
|
|
|
| 749 |
if thinking_depth is not None:
|
| 750 |
depth_idx = thinking_depth.long().clamp(0, self.thinking_config.num_thinking_depths - 1)
|
| 751 |
depth_embedding = self.thinking_embedding(depth_idx) # (batch, hidden_size)
|
| 752 |
+
# N8 FIX: Removed dead depth_bias computation (was computed but never used)
|
| 753 |
+
# Apply thinking_gate as scaling factor, then project to vocab space via lm_head
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
depth_hidden = self.thinking_gate * depth_embedding # (batch, hidden_size)
|
| 755 |
# Add as residual to logits via lm_head projection
|
| 756 |
if hasattr(self.base_model, 'lm_head'):
|
|
@@ -118,57 +118,21 @@ class FusionFullFinetuneDataset(Dataset):
|
|
| 118 |
}
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
| 121 |
def create_local_model(
|
| 122 |
model_size: str = "8B",
|
| 123 |
torch_dtype: torch.dtype = torch.bfloat16,
|
| 124 |
vocab_size_override: Optional[int] = None,
|
| 125 |
):
|
| 126 |
-
"""
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
num_attention_heads=16, num_key_value_heads=8, intermediate_size=5504),
|
| 132 |
-
"1.5B": dict(vocab_size=32000, hidden_size=3072, num_hidden_layers=24,
|
| 133 |
-
num_attention_heads=24, num_key_value_heads=8, intermediate_size=8192),
|
| 134 |
-
"8B": dict(vocab_size=100000, hidden_size=4096, num_hidden_layers=32,
|
| 135 |
-
num_attention_heads=32, num_key_value_heads=8, intermediate_size=11008),
|
| 136 |
-
"14B": dict(vocab_size=100000, hidden_size=5120, num_hidden_layers=40,
|
| 137 |
-
num_attention_heads=40, num_key_value_heads=8, intermediate_size=13824),
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
if model_size not in model_configs:
|
| 141 |
-
raise ValueError(f"不支持的模型大小:{model_size}")
|
| 142 |
-
|
| 143 |
-
config_dict = model_configs[model_size]
|
| 144 |
-
|
| 145 |
-
# S3 fix: override vocab_size to match actual tokenizer
|
| 146 |
-
if vocab_size_override is not None:
|
| 147 |
-
config_dict['vocab_size'] = vocab_size_override
|
| 148 |
-
|
| 149 |
-
common_config = dict(
|
| 150 |
-
block_size=512,
|
| 151 |
-
latent_dim=64,
|
| 152 |
-
window_size=2048,
|
| 153 |
-
sbla_mode="hybrid",
|
| 154 |
-
rms_norm_eps=1e-6,
|
| 155 |
-
rope_theta=10000.0,
|
| 156 |
-
tie_word_embeddings=False,
|
| 157 |
-
enable_thinking_dial=True,
|
| 158 |
-
num_thinking_depths=4,
|
| 159 |
)
|
| 160 |
-
|
| 161 |
-
config = FusionConfig(**config_dict, **common_config)
|
| 162 |
-
|
| 163 |
-
logger.info(f"[create_local_model] 创建 Fusion-{model_size}(随机初始化)")
|
| 164 |
-
logger.info(f" hidden_size={config.hidden_size}, layers={config.num_hidden_layers}, "
|
| 165 |
-
f"heads={config.num_attention_heads}")
|
| 166 |
-
|
| 167 |
-
model = FusionModel(config)
|
| 168 |
-
|
| 169 |
-
total_params = sum(p.numel() for p in model.parameters())
|
| 170 |
-
logger.info(f"[create_local_model] 参数总量:{total_params / 1e9:.2f}B")
|
| 171 |
-
|
| 172 |
return model, config
|
| 173 |
|
| 174 |
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
|
| 121 |
+
from train.model_utils import create_local_model as _create_local_model_from_utils
|
| 122 |
+
|
| 123 |
+
|
| 124 |
def create_local_model(
|
| 125 |
model_size: str = "8B",
|
| 126 |
torch_dtype: torch.dtype = torch.bfloat16,
|
| 127 |
vocab_size_override: Optional[int] = None,
|
| 128 |
):
|
| 129 |
+
"""S4 FIX: Delegate to shared model_utils.create_local_model, preserving API."""
|
| 130 |
+
model = _create_local_model_from_utils(
|
| 131 |
+
model_size=model_size,
|
| 132 |
+
torch_dtype=torch_dtype,
|
| 133 |
+
vocab_size_override=vocab_size_override,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
)
|
| 135 |
+
config = model.config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
return model, config
|
| 137 |
|
| 138 |
|
|
@@ -142,6 +142,9 @@ class FusionDataset(Dataset):
|
|
| 142 |
}
|
| 143 |
|
| 144 |
|
|
|
|
|
|
|
|
|
|
| 145 |
def create_local_model(
|
| 146 |
model_size: str = "8B",
|
| 147 |
quantize: bool = False,
|
|
@@ -149,75 +152,23 @@ def create_local_model(
|
|
| 149 |
load_in_8bit: bool = False,
|
| 150 |
vocab_size_override: int | None = None,
|
| 151 |
):
|
| 152 |
-
"""
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
quantize: 是否量化
|
| 158 |
-
load_in_4bit: 4-bit 量化(NF4)
|
| 159 |
-
load_in_8bit: 8-bit 量化
|
| 160 |
-
vocab_size_override: S3 fix - sync vocab to actual tokenizer size
|
| 161 |
-
"""
|
| 162 |
-
# 模型配置(基于尺寸)
|
| 163 |
-
model_configs = {
|
| 164 |
-
"0.5B": dict(vocab_size=32000, hidden_size=2048, num_hidden_layers=16,
|
| 165 |
-
num_attention_heads=16, num_key_value_heads=8, intermediate_size=5504),
|
| 166 |
-
"1.5B": dict(vocab_size=32000, hidden_size=3072, num_hidden_layers=24,
|
| 167 |
-
num_attention_heads=24, num_key_value_heads=8, intermediate_size=8192),
|
| 168 |
-
"8B": dict(vocab_size=100000, hidden_size=4096, num_hidden_layers=32,
|
| 169 |
-
num_attention_heads=32, num_key_value_heads=8, intermediate_size=11008),
|
| 170 |
-
"14B": dict(vocab_size=100000, hidden_size=5120, num_hidden_layers=40,
|
| 171 |
-
num_attention_heads=40, num_key_value_heads=8, intermediate_size=13824),
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
if model_size not in model_configs:
|
| 175 |
-
raise ValueError(f"不支持的模型大小:{model_size},可选:{list(model_configs.keys())}")
|
| 176 |
-
|
| 177 |
-
config_dict = model_configs[model_size]
|
| 178 |
-
|
| 179 |
-
# S3 fix: override vocab_size to match actual tokenizer
|
| 180 |
-
if vocab_size_override is not None:
|
| 181 |
-
config_dict['vocab_size'] = vocab_size_override
|
| 182 |
-
|
| 183 |
-
# 通用配置
|
| 184 |
-
common_config = dict(
|
| 185 |
-
block_size=512,
|
| 186 |
-
latent_dim=64,
|
| 187 |
-
window_size=2048,
|
| 188 |
-
sbla_mode="hybrid",
|
| 189 |
-
rms_norm_eps=1e-6,
|
| 190 |
-
rope_theta=10000.0,
|
| 191 |
-
tie_word_embeddings=False,
|
| 192 |
-
enable_thinking_dial=True,
|
| 193 |
-
num_thinking_depths=4,
|
| 194 |
)
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
logger.info(f"[create_local_model] 创建 Fusion-{model_size} 模型")
|
| 199 |
-
logger.info(f" vocab_size={config.vocab_size}, hidden_size={config.hidden_size}, "
|
| 200 |
-
f"layers={config.num_hidden_layers}, heads={config.num_attention_heads}")
|
| 201 |
-
|
| 202 |
-
# 创建模型(随机初始化)
|
| 203 |
-
model = FusionModel(config)
|
| 204 |
-
|
| 205 |
-
total_params = sum(p.numel() for p in model.parameters())
|
| 206 |
-
logger.info(f"[create_local_model] 模型参数总量:{total_params / 1e9:.2f}B")
|
| 207 |
-
|
| 208 |
# S-NEW-9 FIX: QLoRA requires proper bitsandbytes integration
|
| 209 |
-
# For local models created from scratch, we can't use HF's AutoModel quantization.
|
| 210 |
-
# Instead, we quantize the model directly with bitsandbytes if available.
|
| 211 |
if quantize:
|
| 212 |
if load_in_4bit:
|
| 213 |
logger.info("[create_local_model] Using 4-bit quantization (QLoRA)")
|
| 214 |
try:
|
| 215 |
import bitsandbytes as bnb
|
| 216 |
-
# S-NEW-12 FIX: Cache module dict to avoid O(n^2) traversal
|
| 217 |
name_to_module = dict(model.named_modules())
|
| 218 |
for name, module in model.named_modules():
|
| 219 |
if isinstance(module, nn.Linear) and not any(x in name for x in ['lora', 'head', 'embed']):
|
| 220 |
-
# Create 4-bit quantized linear (using bitsandbytes nf4)
|
| 221 |
quantized = bnb.nn.Linear4bit(
|
| 222 |
module.in_features,
|
| 223 |
module.out_features,
|
|
@@ -225,7 +176,6 @@ def create_local_model(
|
|
| 225 |
quant_type='nf4',
|
| 226 |
compute_dtype=torch.float16
|
| 227 |
)
|
| 228 |
-
# Replace in model
|
| 229 |
parent_name = '.'.join(name.split('.')[:-1])
|
| 230 |
child_name = name.split('.')[-1]
|
| 231 |
if parent_name:
|
|
@@ -237,14 +187,12 @@ def create_local_model(
|
|
| 237 |
except ImportError:
|
| 238 |
logger.warning("bitsandbytes not installed, 4-bit quantization DISABLED")
|
| 239 |
logger.warning("Model will train in FP32 - install bitsandbytes for true QLoRA")
|
| 240 |
-
# M-NEW-16 FIX: Skip prepare_model_for_kbit_training when bnb unavailable
|
| 241 |
return model, config
|
| 242 |
model = prepare_model_for_kbit_training(model)
|
| 243 |
elif load_in_8bit:
|
| 244 |
logger.info("[create_local_model] Using 8-bit quantization")
|
| 245 |
try:
|
| 246 |
import bitsandbytes as bnb
|
| 247 |
-
# S-NEW-12 FIX: Cache module dict to avoid O(n^2) traversal
|
| 248 |
name_to_module = dict(model.named_modules())
|
| 249 |
for name, module in model.named_modules():
|
| 250 |
if isinstance(module, nn.Linear) and not any(x in name for x in ['lora', 'head', 'embed']):
|
|
@@ -264,10 +212,9 @@ def create_local_model(
|
|
| 264 |
logger.info("[create_local_model] 8-bit quantization applied")
|
| 265 |
except ImportError:
|
| 266 |
logger.warning("bitsandbytes not installed, 8-bit quantization DISABLED")
|
| 267 |
-
# M-NEW-16 FIX: Skip prepare_model_for_kbit_training when bnb unavailable
|
| 268 |
return model, config
|
| 269 |
model = prepare_model_for_kbit_training(model)
|
| 270 |
-
|
| 271 |
return model, config
|
| 272 |
|
| 273 |
|
|
@@ -305,7 +252,13 @@ def apply_lora(
|
|
| 305 |
# L-NEW-2 FIX: Remove "out_proj" (doesn't match model);
|
| 306 |
# FusionModel follows LLaMA naming: q_proj/k_proj/v_proj/o_proj for attention,
|
| 307 |
# gate_proj/up_proj/down_proj for MLP
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
|
| 310 |
logger.info(f"[apply_lora] 应用 LoRA(rank={lora_rank}, alpha={lora_alpha})")
|
| 311 |
logger.info(f"[apply_lora] 目标模块:{target_modules}")
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
|
| 145 |
+
from train.model_utils import create_local_model as _create_local_model_from_utils
|
| 146 |
+
|
| 147 |
+
|
| 148 |
def create_local_model(
|
| 149 |
model_size: str = "8B",
|
| 150 |
quantize: bool = False,
|
|
|
|
| 152 |
load_in_8bit: bool = False,
|
| 153 |
vocab_size_override: int | None = None,
|
| 154 |
):
|
| 155 |
+
"""S4 FIX: Delegate to shared model_utils.create_local_model, then apply quantization."""
|
| 156 |
+
model = _create_local_model_from_utils(
|
| 157 |
+
model_size=model_size,
|
| 158 |
+
torch_dtype=torch.bfloat16,
|
| 159 |
+
vocab_size_override=vocab_size_override,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
)
|
| 161 |
+
config = model.config
|
| 162 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
# S-NEW-9 FIX: QLoRA requires proper bitsandbytes integration
|
|
|
|
|
|
|
| 164 |
if quantize:
|
| 165 |
if load_in_4bit:
|
| 166 |
logger.info("[create_local_model] Using 4-bit quantization (QLoRA)")
|
| 167 |
try:
|
| 168 |
import bitsandbytes as bnb
|
|
|
|
| 169 |
name_to_module = dict(model.named_modules())
|
| 170 |
for name, module in model.named_modules():
|
| 171 |
if isinstance(module, nn.Linear) and not any(x in name for x in ['lora', 'head', 'embed']):
|
|
|
|
| 172 |
quantized = bnb.nn.Linear4bit(
|
| 173 |
module.in_features,
|
| 174 |
module.out_features,
|
|
|
|
| 176 |
quant_type='nf4',
|
| 177 |
compute_dtype=torch.float16
|
| 178 |
)
|
|
|
|
| 179 |
parent_name = '.'.join(name.split('.')[:-1])
|
| 180 |
child_name = name.split('.')[-1]
|
| 181 |
if parent_name:
|
|
|
|
| 187 |
except ImportError:
|
| 188 |
logger.warning("bitsandbytes not installed, 4-bit quantization DISABLED")
|
| 189 |
logger.warning("Model will train in FP32 - install bitsandbytes for true QLoRA")
|
|
|
|
| 190 |
return model, config
|
| 191 |
model = prepare_model_for_kbit_training(model)
|
| 192 |
elif load_in_8bit:
|
| 193 |
logger.info("[create_local_model] Using 8-bit quantization")
|
| 194 |
try:
|
| 195 |
import bitsandbytes as bnb
|
|
|
|
| 196 |
name_to_module = dict(model.named_modules())
|
| 197 |
for name, module in model.named_modules():
|
| 198 |
if isinstance(module, nn.Linear) and not any(x in name for x in ['lora', 'head', 'embed']):
|
|
|
|
| 212 |
logger.info("[create_local_model] 8-bit quantization applied")
|
| 213 |
except ImportError:
|
| 214 |
logger.warning("bitsandbytes not installed, 8-bit quantization DISABLED")
|
|
|
|
| 215 |
return model, config
|
| 216 |
model = prepare_model_for_kbit_training(model)
|
| 217 |
+
|
| 218 |
return model, config
|
| 219 |
|
| 220 |
|
|
|
|
| 252 |
# L-NEW-2 FIX: Remove "out_proj" (doesn't match model);
|
| 253 |
# FusionModel follows LLaMA naming: q_proj/k_proj/v_proj/o_proj for attention,
|
| 254 |
# gate_proj/up_proj/down_proj for MLP
|
| 255 |
+
# S3 FIX: Include SBLAttention latent projections for proper LoRA coverage
|
| 256 |
+
target_modules = [
|
| 257 |
+
"q_proj", "v_proj", "k_proj", "o_proj",
|
| 258 |
+
"gate_proj", "up_proj", "down_proj",
|
| 259 |
+
"latent_q_proj", "latent_k_proj", "latent_v_proj", "latent_out_proj",
|
| 260 |
+
"v_to_hidden_proj",
|
| 261 |
+
]
|
| 262 |
|
| 263 |
logger.info(f"[apply_lora] 应用 LoRA(rank={lora_rank}, alpha={lora_alpha})")
|
| 264 |
logger.info(f"[apply_lora] 目标模块:{target_modules}")
|
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared model creation utilities for Fusion-LLM training scripts.
|
| 2 |
+
|
| 3 |
+
S4 FIX: Extract duplicated create_local_model() from lora_finetune.py and
|
| 4 |
+
full_finetune.py into a single source of truth.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Model size presets
|
| 13 |
+
MODEL_CONFIGS = {
|
| 14 |
+
"0.5B": dict(vocab_size=32000, hidden_size=2048, num_hidden_layers=16,
|
| 15 |
+
num_attention_heads=16, num_key_value_heads=8, intermediate_size=5504),
|
| 16 |
+
"1.5B": dict(vocab_size=32000, hidden_size=3072, num_hidden_layers=24,
|
| 17 |
+
num_attention_heads=24, num_key_value_heads=8, intermediate_size=8192),
|
| 18 |
+
"8B": dict(vocab_size=100000, hidden_size=4096, num_hidden_layers=32,
|
| 19 |
+
num_attention_heads=32, num_key_value_heads=8, intermediate_size=11008),
|
| 20 |
+
"14B": dict(vocab_size=100000, hidden_size=5120, num_hidden_layers=40,
|
| 21 |
+
num_attention_heads=40, num_key_value_heads=8, intermediate_size=13824),
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
COMMON_CONFIG = dict(
|
| 25 |
+
block_size=512,
|
| 26 |
+
latent_dim=64,
|
| 27 |
+
window_size=2048,
|
| 28 |
+
sbla_mode="hybrid",
|
| 29 |
+
rms_norm_eps=1e-6,
|
| 30 |
+
rope_theta=10000.0,
|
| 31 |
+
tie_word_embeddings=False,
|
| 32 |
+
enable_thinking_dial=True,
|
| 33 |
+
num_thinking_depths=4,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_local_model(
|
| 38 |
+
model_size: str = "8B",
|
| 39 |
+
torch_dtype: torch.dtype = torch.bfloat16,
|
| 40 |
+
vocab_size_override: int | None = None,
|
| 41 |
+
):
|
| 42 |
+
"""Create a locally-initialized FusionModel (no pretrained weights required).
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
model_size: One of "0.5B", "1.5B", "8B", "14B"
|
| 46 |
+
torch_dtype: Model dtype (default bfloat16)
|
| 47 |
+
vocab_size_override: Override vocab_size to match actual tokenizer
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
FusionModel instance with random initialization
|
| 51 |
+
"""
|
| 52 |
+
from models.fusion_model import FusionConfig, FusionModel
|
| 53 |
+
|
| 54 |
+
if model_size not in MODEL_CONFIGS:
|
| 55 |
+
raise ValueError(f"Unsupported model size: {model_size}, options: {list(MODEL_CONFIGS.keys())}")
|
| 56 |
+
|
| 57 |
+
config_dict = MODEL_CONFIGS[model_size].copy()
|
| 58 |
+
|
| 59 |
+
if vocab_size_override is not None:
|
| 60 |
+
config_dict['vocab_size'] = vocab_size_override
|
| 61 |
+
|
| 62 |
+
config = FusionConfig(**config_dict, **COMMON_CONFIG)
|
| 63 |
+
|
| 64 |
+
logger.info(f"[create_local_model] Creating Fusion-{model_size} model")
|
| 65 |
+
logger.info(f" vocab_size={config.vocab_size}, hidden_size={config.hidden_size}, "
|
| 66 |
+
f"layers={config.num_hidden_layers}, heads={config.num_attention_heads}")
|
| 67 |
+
|
| 68 |
+
model = FusionModel(config)
|
| 69 |
+
|
| 70 |
+
if torch_dtype is not None:
|
| 71 |
+
model = model.to(torch_dtype)
|
| 72 |
+
|
| 73 |
+
param_count = sum(p.numel() for p in model.parameters())
|
| 74 |
+
logger.info(f" Parameters: {param_count / 1e9:.2f}B")
|
| 75 |
+
|
| 76 |
+
return model
|