Spaces:
Running
v17: fix Thinking Dial training pipeline (N10/N11/N12/N13/N14/N15/N16/N17)
Browse filesCRITICAL (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 +22 -3
- models/thinking_dial.py +108 -13
- tests/test_thinking_dial_integration.py +61 -0
- train/full_finetune.py +1 -2
- train/lora_finetune.py +3 -3
- train/model_utils.py +2 -2
|
@@ -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 |
-
|
| 480 |
-
past_key_values
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
@@ -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
|
| 443 |
-
eos_token_id=self.model.config
|
| 444 |
thinking_depth=thinking_depth,
|
| 445 |
**kwargs,
|
| 446 |
)
|
| 447 |
|
| 448 |
-
# Decode
|
| 449 |
-
if
|
| 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 |
-
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
input_ids=input_ids,
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
temperature=temperature,
|
| 559 |
top_p=top_p,
|
| 560 |
do_sample=True,
|
| 561 |
-
pad_token_id=
|
| 562 |
-
eos_token_id=
|
|
|
|
|
|
|
| 563 |
)
|
| 564 |
-
|
|
|
|
|
|
|
| 565 |
|
| 566 |
-
if
|
| 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
|
| 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 |
# ============================================================
|
|
@@ -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
|
|
@@ -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 |
|
|
@@ -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=
|
| 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:
|
|
@@ -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
|
| 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
|