Transformers
Safetensors
English
mla
deepseek-moe
mtp
custom-code
tinystories
from-scratch
Eval Results (legacy)
Instructions to use nowordsxiaomu/DeepSeek-Flash-Mini with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use nowordsxiaomu/DeepSeek-Flash-Mini with Transformers:
# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("nowordsxiaomu/DeepSeek-Flash-Mini", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| """推理:压缩 KV cache 增量解码 + 基于 MTP 头的自投机解码。 | |
| python generate.py --ckpt checkpoints/best.pt --prompt "在深夜," --spec | |
| 自投机解码(self-speculative decoding)的思路: | |
| MTP 头本来就是训来预测「再下一个」token 的,那它天然就是一个 draft model, | |
| 而且和主干共享 KV cache,几乎不额外花钱。 | |
| 每一轮: | |
| 1. 主干一次前向同时吃 [上一个真 token, 上一轮的草稿 token] | |
| 2. 第 1 个位置的输出告诉我们「真正的下一个 token 应该是什么」→ 用来验草稿 | |
| 3. 草稿被接受 → 这一轮白赚一个 token(第 2 个位置的输出直接就是再下一个) | |
| 草稿被拒绝 → 回滚一格,损失仅一次多余的 KV 写入 | |
| 验收用的是标准 speculative sampling 的接受-重采样规则, | |
| 输出分布和不开投机时**严格一致**,不是近似。 | |
| """ | |
| import argparse | |
| import sys | |
| import time | |
| from typing import Optional, Tuple | |
| import torch | |
| import torch.nn.functional as F | |
| from config import ModelConfig | |
| from dataio.tokenizer import load_tokenizer | |
| from model import DeepSeekFlashMini | |
| # --------------------------------------------------------------- 采样工具 | |
| def filter_logits(logits: torch.Tensor, temperature: float, top_k: int, top_p: float): | |
| """返回过滤+归一化后的概率分布 (1, V)。""" | |
| if temperature <= 0: # 贪心:退化成 one-hot | |
| probs = torch.zeros_like(logits) | |
| probs.scatter_(-1, logits.argmax(-1, keepdim=True), 1.0) | |
| return probs | |
| logits = logits / temperature | |
| if top_k and top_k > 0: | |
| k = min(top_k, logits.size(-1)) | |
| thresh = logits.topk(k, dim=-1)[0][..., -1:] | |
| logits = logits.masked_fill(logits < thresh, float("-inf")) | |
| if top_p and 0 < top_p < 1.0: | |
| sorted_logits, sorted_idx = logits.sort(dim=-1, descending=True) | |
| cum = sorted_logits.softmax(-1).cumsum(-1) | |
| remove = cum - sorted_logits.softmax(-1) > top_p | |
| sorted_logits = sorted_logits.masked_fill(remove, float("-inf")) | |
| logits = torch.empty_like(logits).scatter_(-1, sorted_idx, sorted_logits) | |
| return logits.softmax(-1) | |
| def sample_from(probs: torch.Tensor) -> int: | |
| return int(torch.multinomial(probs, num_samples=1).item()) | |
| # --------------------------------------------------------------- 生成器 | |
| class Generator: | |
| def __init__(self, model: DeepSeekFlashMini, tokenizer, device, max_seq_len=None, | |
| attn_impl: str = "naive"): | |
| self.model = model.eval() | |
| self.tok = tokenizer | |
| self.device = device | |
| self.max_seq_len = max_seq_len or model.cfg.max_seq_len | |
| model.set_attn_impl(attn_impl) | |
| model.setup_cache(1, self.max_seq_len, device, torch.float32) | |
| def _t(self, ids): | |
| return torch.tensor([ids], dtype=torch.long, device=self.device) | |
| def generate(self, prompt: str, max_new_tokens: int = 200, temperature: float = 0.8, | |
| top_k: int = 50, top_p: float = 0.95, speculative: bool = False, | |
| stream: bool = True, stop_on_eos: bool = True) -> Tuple[str, dict]: | |
| ids = self.tok.encode(prompt, bos=True) | |
| keep = max(1, self.max_seq_len - max_new_tokens - 2) | |
| ids = ids[-keep:] or [self.tok.bos_id] | |
| self.eos = self.tok.eos_id if stop_on_eos else -1 | |
| gen = self._spec_loop if speculative else self._plain_loop | |
| t0 = time.time() | |
| out_ids, meta = gen(ids, max_new_tokens, temperature, top_k, top_p, stream) | |
| out_ids = out_ids[:max_new_tokens] # 投机解码一轮可能吐 2 个,这里截齐 | |
| meta["seconds"] = time.time() - t0 | |
| meta["tokens"] = len(out_ids) | |
| meta["tok_per_s"] = len(out_ids) / max(meta["seconds"], 1e-9) | |
| meta["ids"] = out_ids | |
| meta["prompt_ids"] = ids | |
| text = self.tok.decode(ids + out_ids) | |
| return text, meta | |
| # -------------------------------------------------- 普通增量解码 | |
| def _plain_loop(self, ids, max_new, temperature, top_k, top_p, stream): | |
| m = self.model | |
| pos = 0 | |
| pending = list(ids) | |
| out = [] | |
| printed = 0 | |
| while len(out) < max_new: | |
| _, logits, _ = m.forward_trunk(self._t(pending), start_pos=pos) | |
| pos += len(pending) | |
| probs = filter_logits(logits[:, -1].float(), temperature, top_k, top_p) | |
| t = sample_from(probs) | |
| if t == self.eos: | |
| break | |
| out.append(t) | |
| pending = [t] | |
| printed = self._stream(out, printed, stream) | |
| if pos + 2 >= self.max_seq_len: | |
| break | |
| if stream: | |
| print() | |
| return out, {"mode": "plain", "accepted": 0, "rounds": len(out)} | |
| # -------------------------------------------------- MTP 自投机解码 | |
| def _spec_loop(self, ids, max_new, temperature, top_k, top_p, stream): | |
| m = self.model | |
| assert m.cfg.n_mtp > 0, "该模型没有 MTP 头,无法投机解码" | |
| pos = 0 | |
| pending = list(ids) | |
| out = [] | |
| printed = 0 | |
| draft: Optional[int] = None | |
| draft_q: Optional[torch.Tensor] = None | |
| rounds = accepted = 0 | |
| while len(out) < max_new: | |
| T = len(pending) | |
| h, logits, _ = m.forward_trunk(self._t(pending), start_pos=pos) | |
| rounds += 1 | |
| eos_hit = False | |
| if draft is None: | |
| # 首轮(prefill):只出一个真 token | |
| p = filter_logits(logits[:, -1].float(), temperature, top_k, top_p) | |
| t_new = sample_from(p) | |
| pos += T | |
| if t_new == self.eos: | |
| break | |
| out.append(t_new) | |
| next_tokens = pending[1:] + [t_new] | |
| h_chunk, chunk_start = h, pos - T | |
| else: | |
| # pending = [上一个真 token, 草稿],用位置 T-2 的输出来验草稿 | |
| p = filter_logits(logits[:, T - 2].float(), temperature, top_k, top_p) | |
| q = draft_q | |
| ratio = (p[0, draft] / q[0, draft].clamp_min(1e-10)).clamp(max=1.0) | |
| if torch.rand(1, device=ratio.device) < ratio: | |
| # ---- 接受草稿:这一轮吐 2 个 token ---- | |
| accepted += 1 | |
| out.append(draft) | |
| p2 = filter_logits(logits[:, T - 1].float(), temperature, top_k, top_p) | |
| t_new = sample_from(p2) | |
| pos += T | |
| if draft == self.eos or t_new == self.eos: | |
| eos_hit = True | |
| else: | |
| out.append(t_new) | |
| next_tokens = pending[1:] + [t_new] | |
| h_chunk, chunk_start = h, pos - T | |
| else: | |
| # ---- 拒绝:从残差分布 max(0, p-q) 重采样,保证分布无偏 ---- | |
| resid = (p - q).clamp_min(0) | |
| s = resid.sum() | |
| resid = p if s < 1e-9 else resid / s # p==q 的退化情形 | |
| t_new = sample_from(resid) | |
| pos += T - 1 # 回滚草稿占的那一格 | |
| if t_new == self.eos: | |
| eos_hit = True | |
| else: | |
| out.append(t_new) | |
| next_tokens = pending[1:T - 1] + [t_new] | |
| h_chunk, chunk_start = h[:, :T - 1], pos - (T - 1) | |
| printed = self._stream(out, printed, stream) | |
| if eos_hit or pos + 3 >= self.max_seq_len or len(out) >= max_new: | |
| break | |
| # ---- 用 MTP 头造下一轮的草稿(顺带把 MTP 的 cache 补齐)---- | |
| _, mtp_logits, _ = m.mtp_forward( | |
| h_chunk, self._t(next_tokens), start_pos=chunk_start, depth=0) | |
| draft_q = filter_logits(mtp_logits[:, -1].float(), temperature, top_k, top_p) | |
| draft = sample_from(draft_q) | |
| pending = [out[-1], draft] | |
| if stream: | |
| print() | |
| rate = accepted / max(rounds - 1, 1) | |
| return out, {"mode": "speculative", "accepted": accepted, "rounds": rounds, | |
| "accept_rate": rate} | |
| def _stream(self, out, printed, stream): | |
| if not stream: | |
| return printed | |
| text = self.tok.decode(out) | |
| if len(text) > printed: | |
| sys.stdout.write(text[printed:]) | |
| sys.stdout.flush() | |
| printed = len(text) | |
| return printed | |
| # --------------------------------------------------------------- CLI | |
| def load_model(ckpt_path, device): | |
| ck = torch.load(ckpt_path, map_location=device, weights_only=False) | |
| cfg = ModelConfig.from_dict(ck["config"]) | |
| model = DeepSeekFlashMini(cfg).to(device) | |
| model.load_state_dict(ck["model"]) | |
| return model, cfg, ck | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--ckpt", default="checkpoints/best.pt") | |
| ap.add_argument("--tokenizer", default="") | |
| ap.add_argument("--prompt", default="在深夜,") | |
| ap.add_argument("--max-new-tokens", type=int, default=160) | |
| ap.add_argument("--temperature", type=float, default=0.8) | |
| ap.add_argument("--top-k", type=int, default=50) | |
| ap.add_argument("--top-p", type=float, default=0.95) | |
| ap.add_argument("--spec", action="store_true", help="开启 MTP 自投机解码") | |
| ap.add_argument("--attn", default="naive", choices=["naive", "absorb"]) | |
| ap.add_argument("--device", default="auto") | |
| ap.add_argument("--seed", type=int, default=0) | |
| args = ap.parse_args() | |
| torch.manual_seed(args.seed) | |
| device = (torch.device("cuda") if torch.cuda.is_available() else | |
| torch.device("mps") if torch.backends.mps.is_available() else | |
| torch.device("cpu")) if args.device == "auto" else torch.device(args.device) | |
| model, cfg, ck = load_model(args.ckpt, device) | |
| tok_path = args.tokenizer or f"{ck.get('data_dir', 'data')}/tokenizer.json" | |
| tok = load_tokenizer(tok_path) | |
| gen = Generator(model, tok, device, attn_impl=args.attn) | |
| print(f"--- {'MTP 投机解码' if args.spec else '普通解码'} | {args.attn} 注意力 ---") | |
| print(args.prompt, end="") | |
| _, meta = gen.generate(args.prompt, args.max_new_tokens, args.temperature, | |
| args.top_k, args.top_p, speculative=args.spec) | |
| line = (f"[{meta['tokens']} tokens / {meta['seconds']:.2f}s = " | |
| f"{meta['tok_per_s']:.1f} tok/s") | |
| if args.spec: | |
| line += f" | 草稿接受率 {meta['accept_rate']*100:.0f}%" | |
| print(line + "]") | |
| if __name__ == "__main__": | |
| main() | |