"""M2 — Extended Thinking Budget for ARCHON. Pattern Anthropic Claude 3.7+: separate `...` budget, not shown to user but computed via extra forward passes. ARCHON ChatML v3 already has `task_type` special token (32005). We add `` and `` as user-prompt-side delimiters, OR train SFT with thinking traces from DeepSeek R1 distillation. Bénéfice (Anthropic): GPQA 84.8% @ 64K thinking budget on Sonnet 4.6. Pour ARCHON 282M: cible amélioration log-scale +5-10% sur math/code @ 8K thinking. """ from __future__ import annotations from dataclasses import dataclass import torch @dataclass class ExtendedThinkingConfig: """Thinking budget hyper-params.""" thinking_budget_tokens: int = 8192 # ARCHON-scale (vs 64K Claude) final_answer_max_tokens: int = 1024 thinking_start_token: str = "" thinking_end_token: str = "" answer_start_token: str = "" answer_end_token: str = "" # Sampling for thinking (more exploration) thinking_temperature: float = 0.95 thinking_top_p: float = 0.95 # Sampling for final (more deterministic) final_temperature: float = 0.3 final_top_p: float = 0.8 @torch.no_grad() def generate_with_thinking( model, tokenizer, prompt: str, cfg: ExtendedThinkingConfig = ExtendedThinkingConfig(), ) -> dict: """Two-phase generation: thinking + final answer. Phase 1: generate `...` (up to budget tokens) Phase 2: generate `...` from prompt + thinking Returns: {"thinking": str, "final": str, "thinking_tokens": int, "final_tokens": int} """ # Build prompt full_prompt = f"{prompt}\n{cfg.thinking_start_token}\n" input_ids = tokenizer(full_prompt, return_tensors="pt").input_ids.to(next(model.parameters()).device) # Phase 1: thinking think_out = model.generate( input_ids, max_new_tokens=cfg.thinking_budget_tokens, temperature=cfg.thinking_temperature, top_p=cfg.thinking_top_p, ) think_text = tokenizer.decode(think_out[0][input_ids.shape[-1]:], skip_special_tokens=False) # Truncate at end_marker = cfg.thinking_end_token if end_marker in think_text: think_text = think_text.split(end_marker)[0] n_think_tokens = think_out.shape[-1] - input_ids.shape[-1] # Phase 2: final answer conditioned on thinking final_prompt = (full_prompt + think_text + cfg.thinking_end_token + "\n" + cfg.answer_start_token + "\n") final_input_ids = tokenizer(final_prompt, return_tensors="pt").input_ids.to(input_ids.device) final_out = model.generate( final_input_ids, max_new_tokens=cfg.final_answer_max_tokens, temperature=cfg.final_temperature, top_p=cfg.final_top_p, ) final_text = tokenizer.decode( final_out[0][final_input_ids.shape[-1]:], skip_special_tokens=False ) if cfg.answer_end_token in final_text: final_text = final_text.split(cfg.answer_end_token)[0] n_final_tokens = final_out.shape[-1] - final_input_ids.shape[-1] return { "thinking": think_text.strip(), "final": final_text.strip(), "thinking_tokens": n_think_tokens, "final_tokens": n_final_tokens, } def thinking_sft_data_recipe() -> str: """Doc: how to SFT ARCHON to use thinking properly.""" return """ SFT recipe for extended thinking: 1. Distill 10K DeepSeek R1 traces on math/code/reasoning prompts (DeepSeek-R1 Distill-Qwen-1.5B traces are CC-BY freely available) 2. Reformat as ChatML: <|im_start|>user {prompt} <|im_end|> <|im_start|>assistant {R1_trace_truncated_8K} {final_answer} <|im_end|> 3. Mix 15-20% with regular SFT v2 data (avoid catastrophic forgetting) 4. Train 1-2K extra steps on ARCHON SFT v2 -> M2-enabled v2.1 """ if __name__ == "__main__": cfg = ExtendedThinkingConfig() print(f"[M2 ExtThink] budget={cfg.thinking_budget_tokens} tokens") print(thinking_sft_data_recipe())