zhan1206 commited on
Commit
65bdd96
·
1 Parent(s): 2c578a9

v16: fix critical audit defects (N6/N7/N8/N9 + S3/S4/M2/M5)

Browse files

CRITICAL:
- 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 CHANGED
@@ -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, num_heads, seq_len, head_dim) or (batch, num_kv_heads, seq_len, head_dim)
154
- cos: (seq_len, head_dim) cosine part of rotary embedding
155
- sin: (seq_len, head_dim) sine part of rotary embedding
156
- position_ids: optional position ids (unused, for API compat)
157
 
158
  Returns:
159
  Tuple of (q_embed, k_embed) with rotary position encoding applied.
160
  """
161
- # cos/sin: (seq_len, head_dim) -> (1, 1, seq_len, head_dim)
162
- cos = cos.unsqueeze(0).unsqueeze(0)
163
- sin = sin.unsqueeze(0).unsqueeze(0)
 
 
 
 
 
 
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
- # Apply RoPE to Q (full position range) and K (full position range)
254
- Q, K = apply_rotary_pos_emb(Q, K, cos, sin)
 
 
 
 
 
 
 
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.
models/sbla_attention.py CHANGED
@@ -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
- # Compute latent query for the single new token
397
- # V_current is the single-step V before KV concat: (B, num_kv_heads, 1, kv_head_dim)
398
- # After _repeat_kv on V_current it would be (B, num_heads, 1, head_dim)
399
- V_current_expanded = self._repeat_kv(V_current, self.num_kv_groups)
400
- V_reshaped_inc = V_current_expanded.transpose(1, 2).contiguous().view(batch_size, seq_len, -1)
401
- hidden_approx_inc = self.v_to_hidden_proj(V_reshaped_inc) # (batch, 1, hidden_size)
402
- blk_q_inc = self.latent_q_proj(hidden_approx_inc) # (batch, 1, latent_dim)
403
- # Attend to cached block keys/values
404
- latent_attn_scores = torch.matmul(
405
- blk_q_inc, cached_k.transpose(-1, -2)
406
- ) / math.sqrt(self.latent_dim)
407
- # Causal: new token can attend to all blocks
408
- latent_attn_probs = F.softmax(latent_attn_scores, dim=-1)
409
- latent_attn_probs = self.dropout(latent_attn_probs)
410
- latent_context = torch.matmul(latent_attn_probs, cached_v) # (batch, 1, latent_dim)
411
- latent_output = self.latent_out_proj(latent_context) # (batch, 1, hidden_size)
412
- gate_value = torch.sigmoid(self.gate)
413
- output = output_std + gate_value * latent_output
 
 
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
 
models/thinking_dial.py CHANGED
@@ -47,8 +47,9 @@ from transformers import PreTrainedModel, GenerationMixin
47
  # ============================================================
48
 
49
  THINK_START = "<|think_depth_"
50
- THINK_END = "|>"
51
- THINK_DEPTH_PATTERN = re.compile(r"<\|think_depth_(\d+)\|>")
 
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}{THINK_END}"
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
- # Project depth embedding to vocabulary space via tied weights
749
- depth_bias = nn.functional.linear(
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'):
train/full_finetune.py CHANGED
@@ -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
- 创建本地 FusionModel(无需预训练权重)
128
- """
129
- model_configs = {
130
- "0.5B": dict(vocab_size=32000, hidden_size=2048, num_hidden_layers=16,
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
 
train/lora_finetune.py CHANGED
@@ -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
- 创建本地 FusionModel(无需预训练权重)
154
-
155
- 参数:
156
- model_size: "0.5B", "1.5B", "8B", "14B"
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
- config = FusionConfig(**config_dict, **common_config)
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
- target_modules = ["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
 
 
 
 
 
 
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}")
train/model_utils.py ADDED
@@ -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