zhan1206 commited on
Commit
2e32205
·
1 Parent(s): 488ee46

v17: fix Thinking Dial training pipeline (N10/N11/N12/N13/N14/N15/N16/N17)

Browse files

CRITICAL (3 fixes):
- N10: FusionModel.generate() now accepts logits_hook and thinking_depth,
applying depth-dependent logit bias at each generation step via the hook.
FusionModel.forward() accepts thinking_depth parameter.
- N11: ThinkingDialModel now has generate() method that creates a logits_hook
from thinking_embedding + gate, making it compatible with GRPOTrainer.
- N12: generate_samples() now passes thinking_depth through the generation
pipeline. train_step() -> generate_samples() -> generate() all propagate it.

SEVERE (2 fixes):
- N13: lora_finetune.py create_local_model() now accepts torch_dtype parameter
instead of hardcoding bfloat16 (breaks on non-bf16 GPUs like GTX 1080).
- N14: THINK_DEPTH_PATTERN changed from \\d (single digit) to \\d+ for
forward compatibility with depth >= 10.

MEDIUM (3 fixes):
- N15: model_utils.create_local_model() now returns (model, config) tuple
matching the wrapper API in lora/full_finetune.py.
- N16: GRPOTrainer now accepts tokenizer in __init__(), removed dead
hasattr(self, 'tokenizer') branches. Config access uses getattr() for
pad_token_id/eos_token_id (not in FusionConfig).
- N17: generate_samples() now prefills once and clones KV cache per sample,
avoiding redundant prefill computation. FusionModel.generate() accepts
past_key_values parameter for KV cache reuse.

New test: tests/test_thinking_dial_integration.py (4 tests for N10/N11/N12/N17)
All 16 tests pass.

models/fusion_model.py CHANGED
@@ -371,6 +371,7 @@ class FusionModel(PreTrainedModel, GenerationMixin):
371
  inputs_embeds: Optional[torch.Tensor] = None,
372
  use_cache: Optional[bool] = None,
373
  position_ids: Optional[torch.Tensor] = None, # [M6 FIX] Added
 
374
  return_dict: Optional[bool] = True,
375
  **kwargs,
376
  ) -> CausalLMOutputWithPast:
@@ -378,6 +379,9 @@ class FusionModel(PreTrainedModel, GenerationMixin):
378
  # [M6 FIX] Extract position_ids from kwargs if not passed explicitly
379
  if 'position_ids' in kwargs:
380
  position_ids = kwargs.pop('position_ids')
 
 
 
381
  # else: keep the explicit position_ids parameter
382
 
383
  # Embeddings
@@ -446,6 +450,8 @@ class FusionModel(PreTrainedModel, GenerationMixin):
446
  pad_token_id: Optional[int] = None,
447
  eos_token_id: Optional[int] = None,
448
  return_dict_in_generate: bool = False,
 
 
449
  **kwargs,
450
  ) -> CausalLMOutputWithPast:
451
  """Generate text with KV cache and SBLA incremental support.
@@ -476,9 +482,17 @@ class FusionModel(PreTrainedModel, GenerationMixin):
476
  eos_token_id = eos_token_id or getattr(self.config, "eos_token_id", None)
477
 
478
  self.eval()
479
- generated = input_ids.clone()
480
- past_key_values = None
481
- past_seq_len = 0 # [M1 FIX] Track position for RoPE
 
 
 
 
 
 
 
 
482
 
483
  for _ in range(max_new_tokens):
484
  if past_key_values is not None:
@@ -498,11 +512,16 @@ class FusionModel(PreTrainedModel, GenerationMixin):
498
  use_cache=True,
499
  return_dict=True,
500
  position_ids=position_ids, # [M1 FIX]
 
501
  )
502
 
503
  logits = outputs.logits
504
  past_key_values = outputs.past_key_values
505
 
 
 
 
 
506
  next_token_logits = logits[:, -1, :] / max(temperature, 1e-8)
507
 
508
  if do_sample and top_p < 1.0:
 
371
  inputs_embeds: Optional[torch.Tensor] = None,
372
  use_cache: Optional[bool] = None,
373
  position_ids: Optional[torch.Tensor] = None, # [M6 FIX] Added
374
+ thinking_depth: Optional[torch.Tensor] = None, # N10 FIX: Thinking Dial depth
375
  return_dict: Optional[bool] = True,
376
  **kwargs,
377
  ) -> CausalLMOutputWithPast:
 
379
  # [M6 FIX] Extract position_ids from kwargs if not passed explicitly
380
  if 'position_ids' in kwargs:
381
  position_ids = kwargs.pop('position_ids')
382
+ # N10 FIX: Extract thinking_depth from kwargs if not passed explicitly
383
+ if 'thinking_depth' in kwargs:
384
+ thinking_depth = kwargs.pop('thinking_depth')
385
  # else: keep the explicit position_ids parameter
386
 
387
  # Embeddings
 
450
  pad_token_id: Optional[int] = None,
451
  eos_token_id: Optional[int] = None,
452
  return_dict_in_generate: bool = False,
453
+ logits_hook: Optional[callable] = None, # N10 FIX: Hook for ThinkingDial logit bias
454
+ past_key_values: Optional[Tuple] = None, # N17 FIX: Accept pre-computed KV cache
455
  **kwargs,
456
  ) -> CausalLMOutputWithPast:
457
  """Generate text with KV cache and SBLA incremental support.
 
482
  eos_token_id = eos_token_id or getattr(self.config, "eos_token_id", None)
483
 
484
  self.eval()
485
+ # N17 FIX: Support pre-computed KV cache for generate_samples reuse
486
+ if past_key_values is not None:
487
+ past_seq_len = past_key_values[0][0].shape[2] # K shape: (B, H, S, D)
488
+ generated = input_ids
489
+ else:
490
+ past_seq_len = 0 # [M1 FIX] Track position for RoPE
491
+ generated = input_ids.clone()
492
+
493
+ # N10 FIX: Extract thinking_depth from kwargs for forwarding
494
+ thinking_depth = kwargs.pop('thinking_depth', None)
495
+ logits_hook = kwargs.pop('logits_hook', logits_hook)
496
 
497
  for _ in range(max_new_tokens):
498
  if past_key_values is not None:
 
512
  use_cache=True,
513
  return_dict=True,
514
  position_ids=position_ids, # [M1 FIX]
515
+ thinking_depth=thinking_depth, # N10 FIX
516
  )
517
 
518
  logits = outputs.logits
519
  past_key_values = outputs.past_key_values
520
 
521
+ # N10 FIX: Apply logits hook if provided (for ThinkingDialModel depth bias)
522
+ if logits_hook is not None:
523
+ logits = logits_hook(logits)
524
+
525
  next_token_logits = logits[:, -1, :] / max(temperature, 1e-8)
526
 
527
  if do_sample and top_p < 1.0:
models/thinking_dial.py CHANGED
@@ -49,7 +49,7 @@ from transformers import PreTrainedModel, GenerationMixin
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 = {
@@ -323,10 +323,12 @@ class GRPOTrainer:
323
  model: PreTrainedModel,
324
  grpo_config: Optional[GRPOConfig] = None,
325
  thinking_config: Optional[ThinkingConfig] = None,
 
326
  ):
327
  self.model = model
328
  self.grpo_config = grpo_config or GRPOConfig()
329
  self.thinking_config = thinking_config or ThinkingConfig()
 
330
 
331
  # 优化器
332
  self.optimizer = None
@@ -439,14 +441,14 @@ class GRPOTrainer:
439
  temperature=temperature,
440
  top_p=top_p,
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
 
448
- # Decode
449
- if hasattr(self, 'tokenizer') and self.tokenizer is not None:
450
  texts = self.tokenizer.batch_decode(outputs, skip_special_tokens=True)
451
  else:
452
  texts = [f"[generated_ids shape: {outputs.shape}]" for _ in outputs]
@@ -551,19 +553,61 @@ class GRPOTrainer:
551
  all_ids = []
552
  all_texts = []
553
 
554
- for _ in range(num_samples):
555
- outputs = self.model.generate(
 
 
 
 
556
  input_ids=input_ids,
557
- max_new_tokens=max_new_tokens,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  temperature=temperature,
559
  top_p=top_p,
560
  do_sample=True,
561
- pad_token_id=self.model.config.pad_token_id or 0,
562
- eos_token_id=self.model.config.eos_token_id or 1,
 
 
563
  )
564
- all_ids.append(outputs)
 
 
565
 
566
- if hasattr(self, 'tokenizer') and self.tokenizer is not None:
567
  texts = self.tokenizer.batch_decode(outputs, skip_special_tokens=True)
568
  all_texts.extend(texts)
569
 
@@ -610,7 +654,7 @@ class GRPOTrainer:
610
  for i, text in enumerate(generated_texts):
611
  prompt_idx = i // num_samples
612
  prompt_text = ""
613
- if hasattr(self, 'tokenizer') and self.tokenizer is not None:
614
  prompt_text = self.tokenizer.decode(input_ids[prompt_idx], skip_special_tokens=True)
615
  reward = self.compute_reward(prompt_text, text, reward_fn)
616
  rewards_list.append(reward)
@@ -699,6 +743,9 @@ class ThinkingDialModel(nn.Module):
699
 
700
  在基础模型上添加 Thinking Dial 控制能力。
701
  通过额外的 embedding 层学习推理深度表示。
 
 
 
702
  """
703
 
704
  def __init__(
@@ -766,6 +813,54 @@ class ThinkingDialModel(nn.Module):
766
  )
767
 
768
  return base_outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
769
 
770
 
771
  # ============================================================
 
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+)\|") # N14 FIX: \d+ for future depth>=10
53
 
54
  # Depth 0-3 的描述
55
  THINK_DEPTH_DESCRIPTIONS = {
 
323
  model: PreTrainedModel,
324
  grpo_config: Optional[GRPOConfig] = None,
325
  thinking_config: Optional[ThinkingConfig] = None,
326
+ tokenizer=None, # N16 FIX: Accept tokenizer for decode
327
  ):
328
  self.model = model
329
  self.grpo_config = grpo_config or GRPOConfig()
330
  self.thinking_config = thinking_config or ThinkingConfig()
331
+ self.tokenizer = tokenizer # N16 FIX
332
 
333
  # 优化器
334
  self.optimizer = None
 
441
  temperature=temperature,
442
  top_p=top_p,
443
  do_sample=True,
444
+ pad_token_id=getattr(self.model.config, 'pad_token_id', None) or 0,
445
+ eos_token_id=getattr(self.model.config, 'eos_token_id', None) or 1,
446
  thinking_depth=thinking_depth,
447
  **kwargs,
448
  )
449
 
450
+ # Decode - N16 FIX: Use self.tokenizer if available
451
+ if self.tokenizer is not None:
452
  texts = self.tokenizer.batch_decode(outputs, skip_special_tokens=True)
453
  else:
454
  texts = [f"[generated_ids shape: {outputs.shape}]" for _ in outputs]
 
553
  all_ids = []
554
  all_texts = []
555
 
556
+ # N17 FIX: Prefill once, then clone KV cache for each sample
557
+ # Determine the actual generation model (unwrap ThinkingDialModel if needed)
558
+ gen_model = self.model.base_model if hasattr(self.model, 'base_model') else self.model
559
+
560
+ with torch.no_grad():
561
+ prefill_outputs = gen_model.forward(
562
  input_ids=input_ids,
563
+ use_cache=True,
564
+ )
565
+ base_kv = prefill_outputs.past_key_values
566
+ first_logits = prefill_outputs.logits[:, -1, :] # (B, vocab)
567
+
568
+ # N10/N12: Build logits_hook for thinking_depth if using ThinkingDialModel
569
+ logits_hook_arg = None
570
+ if thinking_depth is not None and hasattr(self.model, 'thinking_embedding'):
571
+ if isinstance(thinking_depth, int):
572
+ thinking_depth_t = torch.tensor(
573
+ [thinking_depth] * input_ids.shape[0], device=input_ids.device,
574
+ )
575
+ else:
576
+ thinking_depth_t = thinking_depth
577
+ depth_idx = thinking_depth_t.long().clamp(0, self.model.thinking_config.num_thinking_depths - 1)
578
+ depth_embedding = self.model.thinking_embedding(depth_idx)
579
+ depth_hidden = self.model.thinking_gate * depth_embedding
580
+ vocab_bias = gen_model.lm_head(depth_hidden).unsqueeze(1) # (B, 1, vocab)
581
+ def thinking_logits_hook(logits):
582
+ return logits + vocab_bias
583
+ logits_hook_arg = thinking_logits_hook
584
+
585
+ for _ in range(num_samples):
586
+ # Sample first token from prefill logits
587
+ first_logits_scaled = first_logits / temperature
588
+ probs = F.softmax(first_logits_scaled, dim=-1)
589
+ first_token = torch.multinomial(probs, num_samples=1) # (B, 1)
590
+
591
+ # Clone KV cache for this sample (each sample diverges)
592
+ sample_kv = tuple(tuple(t.clone() for t in layer_kv) for layer_kv in base_kv)
593
+
594
+ # Generate rest using pre-computed KV cache
595
+ outputs = gen_model.generate(
596
+ input_ids=first_token,
597
+ max_new_tokens=max_new_tokens - 1,
598
  temperature=temperature,
599
  top_p=top_p,
600
  do_sample=True,
601
+ pad_token_id=getattr(gen_model.config, 'pad_token_id', None) or 0,
602
+ eos_token_id=getattr(gen_model.config, 'eos_token_id', None) or 1,
603
+ past_key_values=sample_kv, # N17 FIX: Reuse prefilled KV cache
604
+ logits_hook=logits_hook_arg,
605
  )
606
+ # outputs already includes first_token at position 0, prepend original input_ids
607
+ full_output = torch.cat([input_ids, outputs], dim=1)
608
+ all_ids.append(full_output)
609
 
610
+ if self.tokenizer is not None: # N16 FIX
611
  texts = self.tokenizer.batch_decode(outputs, skip_special_tokens=True)
612
  all_texts.extend(texts)
613
 
 
654
  for i, text in enumerate(generated_texts):
655
  prompt_idx = i // num_samples
656
  prompt_text = ""
657
+ if self.tokenizer is not None: # N16 FIX
658
  prompt_text = self.tokenizer.decode(input_ids[prompt_idx], skip_special_tokens=True)
659
  reward = self.compute_reward(prompt_text, text, reward_fn)
660
  rewards_list.append(reward)
 
743
 
744
  在基础模型上添加 Thinking Dial 控制能力。
745
  通过额外的 embedding 层学习推理深度表示。
746
+
747
+ N11 FIX: Now provides generate() method that delegates to base_model.generate()
748
+ with thinking_depth forwarding, making it compatible with GRPOTrainer.
749
  """
750
 
751
  def __init__(
 
813
  )
814
 
815
  return base_outputs
816
+
817
+ # N11 FIX: Provide generate() method for GRPOTrainer compatibility
818
+ @property
819
+ def config(self):
820
+ return self.base_model.config
821
+
822
+ def generate(
823
+ self,
824
+ input_ids: torch.Tensor,
825
+ thinking_depth: Optional[int] = None,
826
+ **kwargs,
827
+ ) -> Any:
828
+ """Generate text with Thinking Dial control.
829
+
830
+ N11 FIX: Now provides generate() by delegating to base_model.generate()
831
+ with a logits_hook that applies depth-dependent bias at each step.
832
+
833
+ Args:
834
+ input_ids: Input token IDs
835
+ thinking_depth: Thinking Dial depth (0-3)
836
+ **kwargs: Additional args forwarded to base_model.generate()
837
+
838
+ Returns:
839
+ Generated token IDs tensor or CausalLMOutputWithPast
840
+ """
841
+ if thinking_depth is not None:
842
+ # Convert scalar depth to tensor for batch
843
+ if isinstance(thinking_depth, int):
844
+ thinking_depth_t = torch.tensor(
845
+ [thinking_depth] * input_ids.shape[0],
846
+ device=input_ids.device,
847
+ )
848
+ else:
849
+ thinking_depth_t = thinking_depth
850
+
851
+ # N10/N11 FIX: Create logits hook that applies thinking bias
852
+ depth_idx = thinking_depth_t.long().clamp(0, self.thinking_config.num_thinking_depths - 1)
853
+ depth_embedding = self.thinking_embedding(depth_idx) # (batch, hidden_size)
854
+ depth_hidden = self.thinking_gate * depth_embedding
855
+ vocab_bias = self.base_model.lm_head(depth_hidden) # (batch, vocab_size)
856
+ vocab_bias_unsqueezed = vocab_bias.unsqueeze(1) # (batch, 1, vocab_size)
857
+
858
+ def thinking_logits_hook(logits):
859
+ return logits + vocab_bias_unsqueezed
860
+
861
+ kwargs['logits_hook'] = thinking_logits_hook
862
+
863
+ return self.base_model.generate(input_ids=input_ids, **kwargs)
864
 
865
 
866
  # ============================================================
tests/test_thinking_dial_integration.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Thinking Dial integration test - verifies N10/N11/N12/N17 fixes."""
2
+ import sys
3
+ import os
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+
6
+ import torch
7
+ import pytest
8
+ from models.fusion_model import FusionConfig, FusionModel
9
+ from models.thinking_dial import ThinkingDialModel, GRPOTrainer, ThinkingConfig
10
+
11
+
12
+ @pytest.fixture
13
+ def setup():
14
+ config = FusionConfig(
15
+ vocab_size=1000, hidden_size=256, num_hidden_layers=2,
16
+ num_attention_heads=4, num_key_value_heads=2, intermediate_size=512,
17
+ block_size=8, latent_dim=16, window_size=64,
18
+ )
19
+ base_model = FusionModel(config)
20
+ base_model.eval()
21
+ td_model = ThinkingDialModel(base_model, ThinkingConfig())
22
+ td_model.eval()
23
+ input_ids = torch.randint(0, 1000, (1, 5))
24
+ return td_model, base_model, input_ids
25
+
26
+
27
+ def test_thinking_dial_model_generate(setup):
28
+ """N11: ThinkingDialModel.generate() exists and works."""
29
+ td_model, _, input_ids = setup
30
+ with torch.no_grad():
31
+ out = td_model.generate(input_ids=input_ids, max_new_tokens=5, thinking_depth=None)
32
+ assert out.shape[0] == 1
33
+
34
+
35
+ def test_thinking_depth_bias_applied(setup):
36
+ """N10: thinking_depth bias is applied during generation."""
37
+ td_model, _, input_ids = setup
38
+ with torch.no_grad():
39
+ out_d0 = td_model.generate(input_ids=input_ids, max_new_tokens=5, thinking_depth=0)
40
+ out_d3 = td_model.generate(input_ids=input_ids, max_new_tokens=5, thinking_depth=3)
41
+ assert out_d0.shape == out_d3.shape
42
+
43
+
44
+ def test_grpo_trainer_generate_with_thinking(setup):
45
+ """N12: GRPOTrainer.generate_with_thinking() passes depth."""
46
+ td_model, _, input_ids = setup
47
+ trainer = GRPOTrainer(td_model)
48
+ with torch.no_grad():
49
+ texts = trainer.generate_with_thinking(input_ids, thinking_depth=2, max_new_tokens=5)
50
+ assert len(texts) == 1
51
+
52
+
53
+ def test_generate_samples_with_depth(setup):
54
+ """N12/N17: generate_samples passes thinking_depth, uses KV cache reuse."""
55
+ td_model, _, input_ids = setup
56
+ trainer = GRPOTrainer(td_model)
57
+ with torch.no_grad():
58
+ ids, texts = trainer.generate_samples(
59
+ input_ids, num_samples=2, thinking_depth=1, max_new_tokens=3,
60
+ )
61
+ assert ids.shape[0] >= 2
train/full_finetune.py CHANGED
@@ -127,12 +127,11 @@ def create_local_model(
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
 
 
127
  vocab_size_override: Optional[int] = None,
128
  ):
129
  """S4 FIX: Delegate to shared model_utils.create_local_model, preserving API."""
130
+ model, config = _create_local_model_from_utils(
131
  model_size=model_size,
132
  torch_dtype=torch_dtype,
133
  vocab_size_override=vocab_size_override,
134
  )
 
135
  return model, config
136
 
137
 
train/lora_finetune.py CHANGED
@@ -150,15 +150,15 @@ def create_local_model(
150
  quantize: bool = False,
151
  load_in_4bit: 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:
 
150
  quantize: bool = False,
151
  load_in_4bit: bool = False,
152
  load_in_8bit: bool = False,
153
+ torch_dtype: torch.dtype = torch.bfloat16, # N13 FIX: expose torch_dtype
154
  vocab_size_override: int | None = None,
155
  ):
156
  """S4 FIX: Delegate to shared model_utils.create_local_model, then apply quantization."""
157
+ model, config = _create_local_model_from_utils(
158
  model_size=model_size,
159
+ torch_dtype=torch_dtype, # N13 FIX
160
  vocab_size_override=vocab_size_override,
161
  )
 
162
 
163
  # S-NEW-9 FIX: QLoRA requires proper bitsandbytes integration
164
  if quantize:
train/model_utils.py CHANGED
@@ -47,7 +47,7 @@ def create_local_model(
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
 
@@ -73,4 +73,4 @@ def create_local_model(
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
 
47
  vocab_size_override: Override vocab_size to match actual tokenizer
48
 
49
  Returns:
50
+ Tuple of (FusionModel instance, FusionConfig) matches train script wrapper API (N15 FIX)
51
  """
52
  from models.fusion_model import FusionConfig, FusionModel
53
 
 
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, config # N15 FIX: Return (model, config) tuple for API consistency