Text Generation
Transformers
Safetensors
English
Japanese
qwen3_5_text
diffusion-lm
masked-diffusion
llada
summerMC-diffusion
experimental
qwen3.5
bart
conversational
Instructions to use summerMC/Diffusion-Qwen3.5-9B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use summerMC/Diffusion-Qwen3.5-9B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="summerMC/Diffusion-Qwen3.5-9B") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("summerMC/Diffusion-Qwen3.5-9B") model = AutoModelForCausalLM.from_pretrained("summerMC/Diffusion-Qwen3.5-9B", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use summerMC/Diffusion-Qwen3.5-9B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "summerMC/Diffusion-Qwen3.5-9B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "summerMC/Diffusion-Qwen3.5-9B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/summerMC/Diffusion-Qwen3.5-9B
- SGLang
How to use summerMC/Diffusion-Qwen3.5-9B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "summerMC/Diffusion-Qwen3.5-9B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "summerMC/Diffusion-Qwen3.5-9B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "summerMC/Diffusion-Qwen3.5-9B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "summerMC/Diffusion-Qwen3.5-9B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use summerMC/Diffusion-Qwen3.5-9B with Docker Model Runner:
docker model run hf.co/summerMC/Diffusion-Qwen3.5-9B
Add dqwen loader code
Browse files- dqwen/__init__.py +1 -0
- dqwen/bart_fusion.py +201 -0
- dqwen/core.py +314 -0
- dqwen/data.py +93 -0
- dqwen/evalutil.py +57 -0
- dqwen/train.py +242 -0
dqwen/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .core import * # noqa
|
dqwen/bart_fusion.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BART (facebook/bart-large, frozen FP32) -> Qwen fusion.
|
| 2 |
+
|
| 3 |
+
Pipeline
|
| 4 |
+
--------
|
| 5 |
+
1. Qwen masked-diffusion generates a DRAFT
|
| 6 |
+
2. BART encodes the draft text -> `bart_states` [B, S, 1024]
|
| 7 |
+
3. Qwen remasks its lowest-confidence draft tokens and re-denoises them while `BartFusion`
|
| 8 |
+
injects the BART context into the response-region input embeddings
|
| 9 |
+
-> generate(prompt) returns (draft, final)
|
| 10 |
+
|
| 11 |
+
dtype contract (this is the fix for the earlier BF16/FP32 mismatch, built in):
|
| 12 |
+
* BART stays FP32 and frozen.
|
| 13 |
+
* BartFusion casts `bart_states` and `qwen_embeds` to *its own weight dtype/device* inside forward(),
|
| 14 |
+
and casts the result back to the dtype of `qwen_embeds`. Works whether fusion is FP32 or BF16.
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import math
|
| 19 |
+
from typing import Optional
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn as nn
|
| 23 |
+
import torch.nn.functional as F
|
| 24 |
+
|
| 25 |
+
from .core import DiffusionLM, diffusion_generate, masked_diffusion_loss
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def fourier_pos(frac: torch.Tensor, dim: int = 64) -> torch.Tensor:
|
| 29 |
+
"""frac: [...,] in roughly [0, 2] -> [..., dim] sin/cos features."""
|
| 30 |
+
n = dim // 2
|
| 31 |
+
freqs = (2.0 ** torch.linspace(0, 6, n, device=frac.device)) * math.pi
|
| 32 |
+
ang = frac[..., None].float() * freqs
|
| 33 |
+
return torch.cat([ang.sin(), ang.cos()], dim=-1)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class BartFusion(nn.Module):
|
| 37 |
+
def __init__(self, qwen_dim: int = 5120, bart_dim: int = 1024, d_inner: int = 1024, n_heads: int = 16):
|
| 38 |
+
super().__init__()
|
| 39 |
+
assert d_inner % n_heads == 0
|
| 40 |
+
self.n_heads, self.d_inner = n_heads, d_inner
|
| 41 |
+
# NOTE: proj[0] is a Linear -> `fusion.proj[0].weight` is valid (used for dtype/device lookups)
|
| 42 |
+
self.proj = nn.Sequential(nn.Linear(bart_dim, qwen_dim), nn.GELU(), nn.Linear(qwen_dim, d_inner))
|
| 43 |
+
self.ln_q = nn.LayerNorm(qwen_dim)
|
| 44 |
+
self.q = nn.Linear(qwen_dim, d_inner)
|
| 45 |
+
self.k = nn.Linear(d_inner, d_inner)
|
| 46 |
+
self.v = nn.Linear(d_inner, d_inner)
|
| 47 |
+
self.o = nn.Linear(d_inner, qwen_dim)
|
| 48 |
+
self.pos_q = nn.Linear(64, d_inner)
|
| 49 |
+
self.pos_k = nn.Linear(64, d_inner)
|
| 50 |
+
self.gate = nn.Parameter(torch.zeros(1)) # tanh-gated, 0 at init => identical to plain Qwen
|
| 51 |
+
|
| 52 |
+
def forward(self, qwen_embeds, bart_states, prefix_len, bart_mask=None, resp_len=None):
|
| 53 |
+
"""qwen_embeds [B,L,H]; bart_states [B,S,bart_dim]; prefix_len: int or LongTensor[B] (prompt length);
|
| 54 |
+
resp_len: int or LongTensor[B] = real (pre-EOS) response length, used for position alignment."""
|
| 55 |
+
w = self.proj[0].weight
|
| 56 |
+
tgt_dtype, tgt_dev = w.dtype, w.device
|
| 57 |
+
out_dtype = qwen_embeds.dtype
|
| 58 |
+
bart_states = bart_states.to(device=tgt_dev, dtype=tgt_dtype) # <- dtype fix
|
| 59 |
+
x = qwen_embeds.to(device=tgt_dev, dtype=tgt_dtype)
|
| 60 |
+
|
| 61 |
+
B, L, _ = x.shape
|
| 62 |
+
S = bart_states.shape[1]
|
| 63 |
+
prefix = torch.as_tensor(prefix_len, device=tgt_dev).reshape(-1).expand(B).long()
|
| 64 |
+
rel = torch.arange(L, device=tgt_dev)[None, :] - prefix[:, None] # [B,L] index inside the response
|
| 65 |
+
is_resp = rel >= 0
|
| 66 |
+
n_real = torch.as_tensor(resp_len if resp_len is not None else (L - prefix),
|
| 67 |
+
device=tgt_dev).reshape(-1).expand(B).float().clamp(min=1)
|
| 68 |
+
fq = rel.clamp(min=0).float() / n_real[:, None]
|
| 69 |
+
if bart_mask is not None:
|
| 70 |
+
s_len = bart_mask.sum(1).clamp(min=1).float().to(tgt_dev)
|
| 71 |
+
else:
|
| 72 |
+
s_len = torch.full((B,), float(S), device=tgt_dev)
|
| 73 |
+
fk = torch.arange(S, device=tgt_dev, dtype=torch.float32)[None, :] / s_len[:, None]
|
| 74 |
+
|
| 75 |
+
ctx = self.proj(bart_states) # [B,S,d_inner]
|
| 76 |
+
q = self.q(self.ln_q(x)) + self.pos_q(fourier_pos(fq).to(tgt_dtype))
|
| 77 |
+
k = self.k(ctx) + self.pos_k(fourier_pos(fk).to(tgt_dtype))
|
| 78 |
+
v = self.v(ctx)
|
| 79 |
+
h = self.n_heads
|
| 80 |
+
sh = lambda t: t.view(B, -1, h, self.d_inner // h).transpose(1, 2)
|
| 81 |
+
am = bart_mask.to(tgt_dev).bool()[:, None, None, :] if bart_mask is not None else None
|
| 82 |
+
a = F.scaled_dot_product_attention(sh(q), sh(k), sh(v), attn_mask=am)
|
| 83 |
+
delta = self.o(a.transpose(1, 2).reshape(B, L, self.d_inner))
|
| 84 |
+
|
| 85 |
+
# keep the injected signal on the same scale as the embeddings it is added to
|
| 86 |
+
d_rms = delta.float().pow(2).mean(-1, keepdim=True).add(1e-6).rsqrt()
|
| 87 |
+
e_rms = x.float().pow(2).mean(-1, keepdim=True).sqrt().detach()
|
| 88 |
+
delta = (delta.float() * d_rms * e_rms) * torch.tanh(self.gate.float())
|
| 89 |
+
delta = delta * is_resp[..., None].float() # prompt positions untouched
|
| 90 |
+
return qwen_embeds + delta.to(device=qwen_embeds.device, dtype=out_dtype)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ------------------------------------------------------------------------------------------
|
| 94 |
+
@torch.no_grad()
|
| 95 |
+
def bart_encode(bart, btok, texts, device, max_len: int = 512):
|
| 96 |
+
enc = btok(list(texts), return_tensors="pt", padding=True, truncation=True, max_length=max_len)
|
| 97 |
+
ids = enc["input_ids"].to(device)
|
| 98 |
+
mask = enc["attention_mask"].to(device)
|
| 99 |
+
enc_dtype = next(bart.parameters()).dtype
|
| 100 |
+
states = bart.get_encoder()(input_ids=ids, attention_mask=mask).last_hidden_state
|
| 101 |
+
return states.to(enc_dtype), mask
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _cut_at_eos(ids_1d: torch.Tensor, eos_id: int) -> int:
|
| 105 |
+
hit = (ids_1d == eos_id).nonzero(as_tuple=True)[0]
|
| 106 |
+
return int(hit[0]) if hit.numel() else int(ids_1d.numel())
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class Pipeline:
|
| 110 |
+
"""Holds everything needed for inference: draft (Qwen diffusion) -> BART -> refine (Qwen + fusion)."""
|
| 111 |
+
|
| 112 |
+
def __init__(self, dlm: DiffusionLM, tok, bart, btok, fusion: BartFusion, eos_id: int, device,
|
| 113 |
+
chat_kwargs: Optional[dict] = None):
|
| 114 |
+
self.dlm, self.tok, self.bart, self.btok, self.fusion = dlm, tok, bart, btok, fusion
|
| 115 |
+
self.eos_id, self.device = eos_id, device
|
| 116 |
+
self.chat_kwargs = chat_kwargs or {"enable_thinking": False}
|
| 117 |
+
|
| 118 |
+
def encode_prompt(self, prompt: str) -> torch.Tensor:
|
| 119 |
+
text = self.tok.apply_chat_template(
|
| 120 |
+
[{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True, **self.chat_kwargs
|
| 121 |
+
)
|
| 122 |
+
return torch.tensor([self.tok(text, add_special_tokens=False)["input_ids"]], device=self.device)
|
| 123 |
+
|
| 124 |
+
def text_of(self, ids_1d: torch.Tensor) -> str:
|
| 125 |
+
n = _cut_at_eos(ids_1d, self.eos_id)
|
| 126 |
+
return self.tok.decode(ids_1d[:n].tolist(), skip_special_tokens=True)
|
| 127 |
+
|
| 128 |
+
@torch.no_grad()
|
| 129 |
+
def generate(self, prompt: str, gen_len=256, steps=64, block_len=32, temperature=0.0,
|
| 130 |
+
refine_ratio=0.3, refine_steps=16, use_bart=True):
|
| 131 |
+
self.dlm.eval(); self.fusion.eval()
|
| 132 |
+
p = self.encode_prompt(prompt)
|
| 133 |
+
draft_ids, conf = diffusion_generate(self.dlm, p, gen_len=gen_len, steps=steps, block_len=block_len,
|
| 134 |
+
temperature=temperature, eos_id=self.eos_id, return_conf=True)
|
| 135 |
+
draft = self.text_of(draft_ids[0])
|
| 136 |
+
if not use_bart:
|
| 137 |
+
return draft, draft
|
| 138 |
+
n_real = _cut_at_eos(draft_ids[0], self.eos_id)
|
| 139 |
+
if n_real == 0:
|
| 140 |
+
return draft, draft
|
| 141 |
+
states, bmask = bart_encode(self.bart, self.btok, [draft if draft.strip() else " "], self.device)
|
| 142 |
+
fusion_fn = lambda emb, plen: self.fusion(emb, states, plen, bart_mask=bmask, resp_len=n_real)
|
| 143 |
+
k = max(1, int(refine_ratio * n_real))
|
| 144 |
+
low = conf[0, :n_real].topk(k, largest=False).indices
|
| 145 |
+
init = draft_ids.clone()
|
| 146 |
+
init[0, low] = self.dlm.mask_id
|
| 147 |
+
# tail after EOS stays EOS (not re-opened)
|
| 148 |
+
final_ids = diffusion_generate(self.dlm, p, steps=refine_steps, block_len=draft_ids.shape[1],
|
| 149 |
+
temperature=temperature, eos_id=self.eos_id, fusion=fusion_fn,
|
| 150 |
+
init_response=init)
|
| 151 |
+
return draft, self.text_of(final_ids[0])
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# ------------------------------------------------------------------------------------------
|
| 155 |
+
@torch.no_grad()
|
| 156 |
+
def make_drafts(dlm: DiffusionLM, ids, resp, t_lo=0.3, t_hi=0.9):
|
| 157 |
+
"""One-shot noisy drafts that look like real diffusion errors: mask at rate t, fill every masked
|
| 158 |
+
position with argmax (+ record the commit-time confidence). Returns (draft_ids, conf)."""
|
| 159 |
+
B, L = ids.shape
|
| 160 |
+
resp = resp.clone(); resp[:, 0] = False
|
| 161 |
+
t = torch.rand(B, device=ids.device) * (t_hi - t_lo) + t_lo
|
| 162 |
+
masked = (torch.rand(B, L, device=ids.device) < t[:, None]) & resp
|
| 163 |
+
noisy = torch.where(masked, torch.full_like(ids, dlm.mask_id), ids)
|
| 164 |
+
h = dlm.hidden(noisy)
|
| 165 |
+
b, j = masked.nonzero(as_tuple=True)
|
| 166 |
+
lg = dlm.head(h[b, j - 1]).float()
|
| 167 |
+
pr = lg.softmax(-1)
|
| 168 |
+
cand = pr.argmax(-1)
|
| 169 |
+
draft = ids.clone()
|
| 170 |
+
conf = torch.ones(B, L, device=ids.device)
|
| 171 |
+
draft[b, j] = cand
|
| 172 |
+
conf[b, j] = pr.gather(-1, cand[:, None]).squeeze(-1)
|
| 173 |
+
return draft, conf
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def refine_loss(dlm, fusion, bart, btok, tok, ids, resp, plen, eos_id, device, ratio_lo=0.15, ratio_hi=0.6):
|
| 177 |
+
"""Stage-2 loss: remask the lowest-confidence draft tokens, predict ground truth with BART context."""
|
| 178 |
+
draft, conf = make_drafts(dlm, ids, resp)
|
| 179 |
+
B, L = ids.shape
|
| 180 |
+
texts, n_real = [], []
|
| 181 |
+
for i in range(B):
|
| 182 |
+
r = draft[i, plen[i]:]
|
| 183 |
+
n = _cut_at_eos(r, eos_id)
|
| 184 |
+
n_real.append(max(n, 1))
|
| 185 |
+
t = tok.decode(r[:n].tolist(), skip_special_tokens=True)
|
| 186 |
+
texts.append(t if t.strip() else " ")
|
| 187 |
+
states, bmask = bart_encode(bart, btok, texts, device)
|
| 188 |
+
|
| 189 |
+
rho = torch.rand(B, device=device) * (ratio_hi - ratio_lo) + ratio_lo
|
| 190 |
+
masked = torch.zeros(B, L, dtype=torch.bool, device=device)
|
| 191 |
+
for i in range(B):
|
| 192 |
+
lo, hi = int(plen[i]), L
|
| 193 |
+
k = max(1, int(rho[i] * n_real[i]))
|
| 194 |
+
c = conf[i, lo:lo + n_real[i]]
|
| 195 |
+
idx = c.topk(min(k, c.numel()), largest=False).indices + lo
|
| 196 |
+
masked[i, idx] = True
|
| 197 |
+
nr = torch.tensor(n_real, device=device)
|
| 198 |
+
pl = torch.as_tensor(plen, device=device)
|
| 199 |
+
fusion_fn = lambda emb, plen_: fusion(emb, states, plen_, bart_mask=bmask, resp_len=nr)
|
| 200 |
+
return masked_diffusion_loss(dlm, ids, resp, weighting="uniform", fusion=fusion_fn, prefix_len=pl,
|
| 201 |
+
forced_masked=masked, context_ids=draft)
|
dqwen/core.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Masked-diffusion adaptation of Qwen3.5 (hybrid Gated-DeltaNet + full attention).
|
| 2 |
+
|
| 3 |
+
Pieces
|
| 4 |
+
------
|
| 5 |
+
* patch_bidirectional : full-attention layers -> bidirectional, DeltaNet layers -> forward + reverse scan
|
| 6 |
+
(reverse path gated by a zero-initialised per-channel gate, so step 0 == original DeltaNet)
|
| 7 |
+
* DiffusionLM : wrapper with a trainable [MASK] embedding, 4D/2D mask dict builder, hidden-state API
|
| 8 |
+
* masked_diffusion_loss : LLaDA-style masked-token CE with "shift" (position j is predicted from hidden j-1,
|
| 9 |
+
which keeps the AR-pretrained lm_head alignment, as in Dream)
|
| 10 |
+
* diffusion_generate : block-wise (semi-AR) confidence-based parallel unmasking
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import math
|
| 15 |
+
from typing import Callable, Optional
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
|
| 21 |
+
try: # class lookup is done lazily so the module imports even on older transformers
|
| 22 |
+
from transformers.models.qwen3_5.modeling_qwen3_5 import (
|
| 23 |
+
Qwen3_5Attention,
|
| 24 |
+
Qwen3_5GatedDeltaNet,
|
| 25 |
+
Qwen3_5TextModel,
|
| 26 |
+
)
|
| 27 |
+
except Exception as e: # pragma: no cover
|
| 28 |
+
raise ImportError(
|
| 29 |
+
"Qwen3.5 classes not found. Install a recent transformers: pip install -U transformers"
|
| 30 |
+
) from e
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# --------------------------------------------------------------------------------------
|
| 34 |
+
# 1. architecture surgery
|
| 35 |
+
# --------------------------------------------------------------------------------------
|
| 36 |
+
def _patch_deltanet(mod: Qwen3_5GatedDeltaNet, hidden_size: int, bidirectional: bool = True):
|
| 37 |
+
"""Make one GatedDeltaNet layer bidirectional.
|
| 38 |
+
|
| 39 |
+
out = orig(h) + g * flip(orig(flip(h))) (out_proj is linear & bias-free, so this equals
|
| 40 |
+
out_proj(core_f + g * core_r) exactly)
|
| 41 |
+
g is a per-channel fp32 parameter initialised to 0.
|
| 42 |
+
"""
|
| 43 |
+
if getattr(mod, "_dqwen_patched", False):
|
| 44 |
+
return
|
| 45 |
+
orig_forward = mod.forward
|
| 46 |
+
# The 4-bit model has already been dispatched to CUDA before patching.
|
| 47 |
+
# New parameters otherwise default to CPU and cause device mismatch.
|
| 48 |
+
reference = next(mod.parameters())
|
| 49 |
+
mod.rev_gate = nn.Parameter(torch.zeros(hidden_size, device=reference.device, dtype=torch.float32))
|
| 50 |
+
mod._dqwen_patched = True
|
| 51 |
+
|
| 52 |
+
def forward(hidden_states, cache_params=None, attention_mask=None, **kwargs):
|
| 53 |
+
y = orig_forward(hidden_states, cache_params=None, attention_mask=attention_mask, **dict(kwargs))
|
| 54 |
+
if not bidirectional:
|
| 55 |
+
return y
|
| 56 |
+
h_r = hidden_states.flip(1)
|
| 57 |
+
m_r = attention_mask.flip(1) if attention_mask is not None else None
|
| 58 |
+
y_r = orig_forward(h_r, cache_params=None, attention_mask=m_r, **dict(kwargs)).flip(1)
|
| 59 |
+
# The first device-fix only moved rev_gate; y_r may still differ.
|
| 60 |
+
# Move the reverse output to the forward output's device with autograd intact.
|
| 61 |
+
if not isinstance(y, torch.Tensor) or not isinstance(y_r, torch.Tensor):
|
| 62 |
+
raise TypeError(f"DeltaNet outputs: forward={type(y)!r}, reverse={type(y_r)!r}")
|
| 63 |
+
if y.device != y_r.device:
|
| 64 |
+
print(f"[DeltaNet device] forward={y.device}, reverse={y_r.device}, "
|
| 65 |
+
f"gate={mod.rev_gate.device}; aligning reverse output")
|
| 66 |
+
y_r = y_r.to(device=y.device, dtype=y.dtype)
|
| 67 |
+
gate = mod.rev_gate.to(device=y.device, dtype=y.dtype)
|
| 68 |
+
try:
|
| 69 |
+
return y + gate * y_r
|
| 70 |
+
except RuntimeError as exc:
|
| 71 |
+
raise RuntimeError(
|
| 72 |
+
f"DeltaNet fusion failed: forward={y.device}/{y.dtype}/{tuple(y.shape)}, "
|
| 73 |
+
f"reverse={y_r.device}/{y_r.dtype}/{tuple(y_r.shape)}, "
|
| 74 |
+
f"gate={gate.device}/{gate.dtype}/{tuple(gate.shape)}; "
|
| 75 |
+
f"source_gate={mod.rev_gate.device}. Original error: {exc}"
|
| 76 |
+
) from exc
|
| 77 |
+
|
| 78 |
+
mod.forward = forward
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def patch_bidirectional(model: nn.Module, deltanet_bidirectional: bool = True) -> dict:
|
| 82 |
+
"""In-place surgery. Returns simple stats."""
|
| 83 |
+
n_full = n_delta = 0
|
| 84 |
+
text = find_text_model(model)
|
| 85 |
+
hidden = text.config.hidden_size
|
| 86 |
+
for m in text.modules():
|
| 87 |
+
if isinstance(m, Qwen3_5Attention):
|
| 88 |
+
m.is_causal = False
|
| 89 |
+
n_full += 1
|
| 90 |
+
elif isinstance(m, Qwen3_5GatedDeltaNet):
|
| 91 |
+
_patch_deltanet(m, hidden, deltanet_bidirectional)
|
| 92 |
+
n_delta += 1
|
| 93 |
+
return {"full_attention_layers": n_full, "deltanet_layers": n_delta}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def find_text_model(model: nn.Module) -> Qwen3_5TextModel:
|
| 97 |
+
for m in model.modules():
|
| 98 |
+
if isinstance(m, Qwen3_5TextModel):
|
| 99 |
+
return m
|
| 100 |
+
raise ValueError("Qwen3_5TextModel not found inside the given module")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def find_lm_head(model: nn.Module) -> nn.Module:
|
| 104 |
+
for name, m in model.named_modules():
|
| 105 |
+
if name.endswith("lm_head"):
|
| 106 |
+
return m
|
| 107 |
+
raise ValueError("lm_head not found")
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def rev_gate_state(model: nn.Module) -> dict:
|
| 111 |
+
return {n: p.detach().cpu() for n, p in model.named_parameters() if n.endswith("rev_gate")}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def load_rev_gates(model: nn.Module, state: dict):
|
| 115 |
+
params = dict(model.named_parameters())
|
| 116 |
+
missing = []
|
| 117 |
+
for n, v in state.items():
|
| 118 |
+
# tolerate different peft prefixes by matching on suffix after 'layers.'
|
| 119 |
+
key = n if n in params else next((k for k in params if k.endswith(n.split("layers.", 1)[-1]) and "rev_gate" in k), None)
|
| 120 |
+
if key is None:
|
| 121 |
+
missing.append(n)
|
| 122 |
+
continue
|
| 123 |
+
params[key].data.copy_(v.to(params[key].dtype))
|
| 124 |
+
return missing
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# --------------------------------------------------------------------------------------
|
| 128 |
+
# 2. wrapper
|
| 129 |
+
# --------------------------------------------------------------------------------------
|
| 130 |
+
class DiffusionLM(nn.Module):
|
| 131 |
+
"""Wraps a (possibly PEFT-wrapped) Qwen3_5ForCausalLM.
|
| 132 |
+
|
| 133 |
+
`fusion` (optional) is a callable (embeds, prefix_len) -> embeds that injects extra context
|
| 134 |
+
(e.g. BART states) into the input embeddings of the response region.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def __init__(self, lm: nn.Module, mask_id: int, init_mask_from_token: Optional[int] = None):
|
| 138 |
+
super().__init__()
|
| 139 |
+
self.lm = lm
|
| 140 |
+
self.mask_id = int(mask_id)
|
| 141 |
+
self.text = find_text_model(lm)
|
| 142 |
+
self.head = find_lm_head(lm)
|
| 143 |
+
hidden = self.text.config.hidden_size
|
| 144 |
+
emb = self.text.embed_tokens.weight
|
| 145 |
+
init = emb[init_mask_from_token if init_mask_from_token is not None else mask_id].detach().float().clone()
|
| 146 |
+
if init.abs().sum() == 0: # untrained row -> fall back to the mean embedding
|
| 147 |
+
init = emb.detach().float().mean(0)
|
| 148 |
+
self.mask_embed = nn.Parameter(init.to(emb.device))
|
| 149 |
+
self.hidden_size = hidden
|
| 150 |
+
|
| 151 |
+
# ---- masks ----------------------------------------------------------------------
|
| 152 |
+
def build_masks(self, attn2d: Optional[torch.Tensor], dtype=torch.bfloat16):
|
| 153 |
+
if attn2d is None:
|
| 154 |
+
return {"full_attention": None, "linear_attention": None}
|
| 155 |
+
a = attn2d.bool()
|
| 156 |
+
full = (a[:, None, :, None] & a[:, None, None, :]) # [B,1,L,L] True = attend
|
| 157 |
+
# a padded query row would be all False -> NaN in sdpa; let pads attend to themselves
|
| 158 |
+
eye = torch.eye(a.shape[1], dtype=torch.bool, device=a.device)[None, None]
|
| 159 |
+
full = full | (~a[:, None, :, None] & eye)
|
| 160 |
+
if getattr(self.text.config, "_attn_implementation", "sdpa") == "eager":
|
| 161 |
+
neg = torch.finfo(dtype).min
|
| 162 |
+
full = torch.zeros(full.shape, dtype=dtype, device=a.device).masked_fill(~full, neg)
|
| 163 |
+
return {"full_attention": full, "linear_attention": a.long()}
|
| 164 |
+
|
| 165 |
+
# ---- embeddings -----------------------------------------------------------------
|
| 166 |
+
def embed(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 167 |
+
e = self.text.embed_tokens(input_ids)
|
| 168 |
+
is_mask = (input_ids == self.mask_id)[..., None]
|
| 169 |
+
return torch.where(is_mask, self.mask_embed.to(e.dtype)[None, None, :], e)
|
| 170 |
+
|
| 171 |
+
def hidden(self, input_ids, attn2d=None, fusion: Optional[Callable] = None, prefix_len=None):
|
| 172 |
+
emb = self.embed(input_ids)
|
| 173 |
+
if fusion is not None:
|
| 174 |
+
emb = fusion(emb, prefix_len)
|
| 175 |
+
out = self.text(
|
| 176 |
+
inputs_embeds=emb,
|
| 177 |
+
attention_mask=self.build_masks(attn2d, emb.dtype),
|
| 178 |
+
use_cache=False,
|
| 179 |
+
)
|
| 180 |
+
return out.last_hidden_state
|
| 181 |
+
|
| 182 |
+
def logits_at(self, h: torch.Tensor) -> torch.Tensor:
|
| 183 |
+
return self.head(h)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# --------------------------------------------------------------------------------------
|
| 187 |
+
# 3. loss
|
| 188 |
+
# --------------------------------------------------------------------------------------
|
| 189 |
+
def _chunked_ce(head, hs: torch.Tensor, targets: torch.Tensor, chunk: int = 1024):
|
| 190 |
+
"""CE per token without materialising [N, V] logits for all N at once."""
|
| 191 |
+
outs = []
|
| 192 |
+
for i in range(0, hs.shape[0], chunk):
|
| 193 |
+
lg = head(hs[i : i + chunk]).float()
|
| 194 |
+
outs.append(F.cross_entropy(lg, targets[i : i + chunk], reduction="none"))
|
| 195 |
+
return torch.cat(outs) if outs else hs.new_zeros(0)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def masked_diffusion_loss(
|
| 199 |
+
dlm: DiffusionLM,
|
| 200 |
+
input_ids: torch.Tensor, # [B, L] clean tokens (prompt + response + EOS padding)
|
| 201 |
+
resp_mask: torch.Tensor, # [B, L] bool, True where the token belongs to the response (may be masked)
|
| 202 |
+
attn2d: Optional[torch.Tensor] = None,
|
| 203 |
+
t_min: float = 0.05,
|
| 204 |
+
weighting: str = "elbo", # "elbo" -> 1/t (LLaDA/Dream), "uniform" -> 1
|
| 205 |
+
fusion: Optional[Callable] = None,
|
| 206 |
+
prefix_len=None,
|
| 207 |
+
forced_masked: Optional[torch.Tensor] = None, # if given, mask exactly this set (used for refine-stage training)
|
| 208 |
+
context_ids: Optional[torch.Tensor] = None, # tokens shown at un-masked positions (draft); targets stay `input_ids`
|
| 209 |
+
):
|
| 210 |
+
B, L = input_ids.shape
|
| 211 |
+
dev = input_ids.device
|
| 212 |
+
resp_mask = resp_mask.clone()
|
| 213 |
+
resp_mask[:, 0] = False # position 0 has no left neighbour to predict it (shift)
|
| 214 |
+
|
| 215 |
+
if forced_masked is None:
|
| 216 |
+
t = torch.rand(B, device=dev) * (1 - t_min) + t_min
|
| 217 |
+
masked = (torch.rand(B, L, device=dev) < t[:, None]) & resp_mask
|
| 218 |
+
# guarantee >=1 masked token per row
|
| 219 |
+
none = masked.sum(1) == 0
|
| 220 |
+
if none.any():
|
| 221 |
+
idx = torch.multinomial(resp_mask[none].float() + 1e-9, 1).squeeze(1)
|
| 222 |
+
rows = none.nonzero(as_tuple=True)[0]
|
| 223 |
+
masked[rows, idx] = True
|
| 224 |
+
else:
|
| 225 |
+
masked = forced_masked & resp_mask
|
| 226 |
+
t = masked.sum(1).float() / resp_mask.sum(1).clamp(min=1)
|
| 227 |
+
t = t.clamp(min=t_min)
|
| 228 |
+
|
| 229 |
+
base = input_ids if context_ids is None else context_ids
|
| 230 |
+
noisy = torch.where(masked, torch.full_like(input_ids, dlm.mask_id), base)
|
| 231 |
+
h = dlm.hidden(noisy, attn2d, fusion=fusion, prefix_len=prefix_len)
|
| 232 |
+
|
| 233 |
+
b_idx, j_idx = masked.nonzero(as_tuple=True)
|
| 234 |
+
hs = h[b_idx, j_idx - 1]
|
| 235 |
+
tgt = input_ids[b_idx, j_idx]
|
| 236 |
+
ce = _chunked_ce(dlm.head, hs, tgt)
|
| 237 |
+
|
| 238 |
+
if weighting == "elbo":
|
| 239 |
+
w = 1.0 / t[b_idx]
|
| 240 |
+
else:
|
| 241 |
+
w = torch.ones_like(ce)
|
| 242 |
+
n_resp = resp_mask.sum(1).clamp(min=1).float()
|
| 243 |
+
loss = (w * ce / n_resp[b_idx]).sum() / B
|
| 244 |
+
|
| 245 |
+
return loss, {"ce": ce.mean().detach(), "n_masked": int(masked.sum()), "t": t.mean().detach()}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# --------------------------------------------------------------------------------------
|
| 249 |
+
# 4. sampler
|
| 250 |
+
# --------------------------------------------------------------------------------------
|
| 251 |
+
@torch.no_grad()
|
| 252 |
+
def diffusion_generate(
|
| 253 |
+
dlm: DiffusionLM,
|
| 254 |
+
prompt_ids: torch.Tensor, # [1, P]
|
| 255 |
+
gen_len: int = 256,
|
| 256 |
+
steps: int = 64,
|
| 257 |
+
block_len: int = 32,
|
| 258 |
+
temperature: float = 0.0,
|
| 259 |
+
eos_id: Optional[int] = None,
|
| 260 |
+
fusion: Optional[Callable] = None,
|
| 261 |
+
init_response: Optional[torch.Tensor] = None, # [1, gen_len] tokens; MASK id => to be generated
|
| 262 |
+
return_conf: bool = False,
|
| 263 |
+
):
|
| 264 |
+
"""Block-wise low-confidence remasking (LLaDA). Returns [1, gen_len] response tokens."""
|
| 265 |
+
assert prompt_ids.shape[0] == 1
|
| 266 |
+
dev = prompt_ids.device
|
| 267 |
+
P = prompt_ids.shape[1]
|
| 268 |
+
if init_response is None:
|
| 269 |
+
resp = torch.full((1, gen_len), dlm.mask_id, dtype=torch.long, device=dev)
|
| 270 |
+
else:
|
| 271 |
+
resp = init_response.clone().to(dev)
|
| 272 |
+
gen_len = resp.shape[1]
|
| 273 |
+
x = torch.cat([prompt_ids, resp], dim=1)
|
| 274 |
+
conf_all = torch.zeros(1, gen_len, device=dev)
|
| 275 |
+
|
| 276 |
+
n_blocks = math.ceil(gen_len / block_len)
|
| 277 |
+
steps_per_block = max(1, steps // n_blocks)
|
| 278 |
+
|
| 279 |
+
for b in range(n_blocks):
|
| 280 |
+
lo, hi = P + b * block_len, min(P + (b + 1) * block_len, P + gen_len)
|
| 281 |
+
blk_masked0 = (x[0, lo:hi] == dlm.mask_id).sum().item()
|
| 282 |
+
if blk_masked0 == 0:
|
| 283 |
+
continue
|
| 284 |
+
# tokens to commit per step (spread evenly)
|
| 285 |
+
base, rem = divmod(blk_masked0, steps_per_block)
|
| 286 |
+
schedule = [base + (1 if i < rem else 0) for i in range(steps_per_block)]
|
| 287 |
+
for k in schedule:
|
| 288 |
+
if k == 0:
|
| 289 |
+
continue
|
| 290 |
+
h = dlm.hidden(x, None, fusion=fusion, prefix_len=P)
|
| 291 |
+
pos = torch.arange(lo, hi, device=dev)
|
| 292 |
+
still = x[0, lo:hi] == dlm.mask_id
|
| 293 |
+
pos = pos[still]
|
| 294 |
+
if pos.numel() == 0:
|
| 295 |
+
break
|
| 296 |
+
lg = dlm.head(h[0, pos - 1]).float() # shift: predict j from j-1
|
| 297 |
+
if temperature > 0:
|
| 298 |
+
g = -torch.log(-torch.log(torch.rand_like(lg).clamp_min(1e-10)).clamp_min(1e-10))
|
| 299 |
+
cand = (lg / temperature + g).argmax(-1)
|
| 300 |
+
else:
|
| 301 |
+
cand = lg.argmax(-1)
|
| 302 |
+
prob = F.softmax(lg, -1).gather(-1, cand[:, None]).squeeze(-1)
|
| 303 |
+
kk = min(k, pos.numel())
|
| 304 |
+
top = prob.topk(kk).indices
|
| 305 |
+
x[0, pos[top]] = cand[top]
|
| 306 |
+
conf_all[0, pos[top] - P] = prob[top]
|
| 307 |
+
if eos_id is not None and (x[0, P : hi] == eos_id).any() and not (x[0, P:hi] == dlm.mask_id).any():
|
| 308 |
+
break
|
| 309 |
+
out = x[:, P:].clone()
|
| 310 |
+
if eos_id is not None:
|
| 311 |
+
hit = (out[0] == eos_id).nonzero(as_tuple=True)[0]
|
| 312 |
+
if hit.numel() > 0:
|
| 313 |
+
out[0, hit[0]:] = eos_id # everything after the first EOS is padding
|
| 314 |
+
return (out, conf_all) if return_conf else out
|
dqwen/data.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chat-SFT data -> fixed-length (prompt | response | EOS filler) tensors for masked diffusion.
|
| 2 |
+
|
| 3 |
+
Following LLaDA, the response region is padded with EOS up to the fixed length and those filler tokens are
|
| 4 |
+
part of the response (so the model learns *when to stop* by predicting EOS at the tail).
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import random
|
| 10 |
+
from typing import Iterable, Optional
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def to_messages(row: dict) -> Optional[list]:
|
| 16 |
+
"""Normalise common SFT row layouts to [{'role','content'}, ...] ending with an assistant turn."""
|
| 17 |
+
if "messages" in row and row["messages"]:
|
| 18 |
+
msgs = [{"role": m["role"], "content": m["content"]} for m in row["messages"]]
|
| 19 |
+
elif "conversations" in row and row["conversations"]:
|
| 20 |
+
rm = {"human": "user", "user": "user", "gpt": "assistant", "assistant": "assistant", "system": "system"}
|
| 21 |
+
msgs = [{"role": rm.get(m.get("from", m.get("role")), "user"), "content": m.get("value", m.get("content"))}
|
| 22 |
+
for m in row["conversations"]]
|
| 23 |
+
elif "instruction" in row and ("output" in row or "response" in row):
|
| 24 |
+
q = row["instruction"] + (("\n" + row["input"]) if row.get("input") else "")
|
| 25 |
+
msgs = [{"role": "user", "content": q}, {"role": "assistant", "content": row.get("output", row.get("response"))}]
|
| 26 |
+
elif "question" in row and "answer" in row:
|
| 27 |
+
msgs = [{"role": "user", "content": row["question"]}, {"role": "assistant", "content": row["answer"]}]
|
| 28 |
+
else:
|
| 29 |
+
return None
|
| 30 |
+
if len(msgs) < 2 or msgs[-1]["role"] != "assistant" or not msgs[-1]["content"]:
|
| 31 |
+
return None
|
| 32 |
+
return msgs
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def iter_rows(source: str, config: Optional[str] = None, split: str = "train", n: int = 20000, seed: int = 0) -> Iterable[dict]:
|
| 36 |
+
"""`source` = path to .jsonl, or a Hugging Face dataset id (streamed, so nothing huge is downloaded)."""
|
| 37 |
+
if source.endswith(".jsonl") or source.endswith(".json"):
|
| 38 |
+
with open(source, encoding="utf-8") as f:
|
| 39 |
+
rows = [json.loads(l) for l in f if l.strip()]
|
| 40 |
+
random.Random(seed).shuffle(rows)
|
| 41 |
+
yield from rows[:n]
|
| 42 |
+
return
|
| 43 |
+
from datasets import load_dataset
|
| 44 |
+
ds = load_dataset(source, config, split=split, streaming=True).shuffle(seed=seed, buffer_size=10_000)
|
| 45 |
+
for i, r in enumerate(ds):
|
| 46 |
+
if i >= n:
|
| 47 |
+
break
|
| 48 |
+
yield r
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def build_dataset(tok, rows: Iterable[dict], max_len: int = 512, max_prompt: int = 256,
|
| 52 |
+
eos_token: str = "<|im_end|>", chat_kwargs: Optional[dict] = None, verbose: bool = True):
|
| 53 |
+
chat_kwargs = chat_kwargs or {"enable_thinking": False}
|
| 54 |
+
eos_id = tok.convert_tokens_to_ids(eos_token)
|
| 55 |
+
ids_l, resp_l, plen_l = [], [], []
|
| 56 |
+
seen = kept = 0
|
| 57 |
+
for row in rows:
|
| 58 |
+
seen += 1
|
| 59 |
+
msgs = to_messages(row)
|
| 60 |
+
if msgs is None:
|
| 61 |
+
continue
|
| 62 |
+
ptxt = tok.apply_chat_template(msgs[:-1], tokenize=False, add_generation_prompt=True, **chat_kwargs)
|
| 63 |
+
p = tok(ptxt, add_special_tokens=False)["input_ids"]
|
| 64 |
+
r = tok(msgs[-1]["content"], add_special_tokens=False)["input_ids"] + [eos_id]
|
| 65 |
+
if len(p) > max_prompt or len(p) + len(r) > max_len:
|
| 66 |
+
continue # drop (never truncate: a cut response would lose its EOS)
|
| 67 |
+
pad = max_len - len(p) - len(r)
|
| 68 |
+
seq = p + r + [eos_id] * pad
|
| 69 |
+
rm = [False] * len(p) + [True] * (len(r) + pad)
|
| 70 |
+
ids_l.append(seq); resp_l.append(rm); plen_l.append(len(p)); kept += 1
|
| 71 |
+
if verbose:
|
| 72 |
+
print(f"[data] kept {kept}/{seen} examples (max_len={max_len}, max_prompt={max_prompt})")
|
| 73 |
+
if kept == 0:
|
| 74 |
+
raise ValueError(
|
| 75 |
+
f"No example survived filtering (seen {seen}). The chat template alone may exceed max_prompt="
|
| 76 |
+
f"{max_prompt}; raise max_prompt/max_len or check the dataset columns.")
|
| 77 |
+
return {
|
| 78 |
+
"ids": torch.tensor(ids_l, dtype=torch.long),
|
| 79 |
+
"resp": torch.tensor(resp_l, dtype=torch.bool),
|
| 80 |
+
"plen": torch.tensor(plen_l, dtype=torch.long),
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def batches(ds: dict, batch_size: int, device, seed: int = 0, epochs: int = 10**9):
|
| 85 |
+
g = torch.Generator().manual_seed(seed)
|
| 86 |
+
N = ds["ids"].shape[0]
|
| 87 |
+
if N < batch_size:
|
| 88 |
+
raise ValueError(f"Dataset has {N} examples but batch_size={batch_size}; lower batch_size or add data.")
|
| 89 |
+
for _ in range(epochs):
|
| 90 |
+
perm = torch.randperm(N, generator=g)
|
| 91 |
+
for i in range(0, N - batch_size + 1, batch_size):
|
| 92 |
+
ix = perm[i:i + batch_size]
|
| 93 |
+
yield ds["ids"][ix].to(device), ds["resp"][ix].to(device), ds["plen"][ix].to(device)
|
dqwen/evalutil.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluation: held-out masked-token CE, GSM8K accuracy (draft vs BART-refined)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from .core import DiffusionLM, masked_diffusion_loss
|
| 10 |
+
from .data import batches
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@torch.no_grad()
|
| 14 |
+
def heldout_ce(dlm: DiffusionLM, ds: dict, device, n_batches: int = 10, batch_size: int = 2, rate: float = 0.5, seed: int = 123):
|
| 15 |
+
"""Mean CE (nats/token) on masked response tokens at a fixed masking rate -> lower is better."""
|
| 16 |
+
was = dlm.training
|
| 17 |
+
dlm.eval()
|
| 18 |
+
g = torch.Generator(device="cpu").manual_seed(seed)
|
| 19 |
+
tot, n = 0.0, 0
|
| 20 |
+
for i, (ids, resp, _) in enumerate(batches(ds, batch_size, device, seed=seed, epochs=1)):
|
| 21 |
+
if i >= n_batches:
|
| 22 |
+
break
|
| 23 |
+
r = resp.clone(); r[:, 0] = False
|
| 24 |
+
forced = (torch.rand(ids.shape, generator=g).to(device) < rate) & r
|
| 25 |
+
_, info = masked_diffusion_loss(dlm, ids, resp, forced_masked=forced, weighting="uniform")
|
| 26 |
+
tot += float(info["ce"]); n += 1
|
| 27 |
+
dlm.train(was)
|
| 28 |
+
return tot / max(n, 1)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
_num = re.compile(r"-?\d[\d,]*\.?\d*")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _last_number(text: str) -> Optional[str]:
|
| 35 |
+
m = _num.findall(text.replace("$", ""))
|
| 36 |
+
return m[-1].replace(",", "").rstrip(".") if m else None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def extract_answer(text: str) -> Optional[str]:
|
| 40 |
+
if "####" in text:
|
| 41 |
+
return _last_number(text.split("####")[-1])
|
| 42 |
+
return _last_number(text)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def gsm8k_eval(pipe, n: int = 50, gen_len: int = 256, steps: int = 128, block_len: int = 32, use_bart: bool = True, seed: int = 0):
|
| 46 |
+
from datasets import load_dataset
|
| 47 |
+
ds = load_dataset("openai/gsm8k", "main", split="test").shuffle(seed=seed).select(range(n))
|
| 48 |
+
ok_draft = ok_final = 0
|
| 49 |
+
for i, row in enumerate(ds):
|
| 50 |
+
gold = row["answer"].split("####")[-1].strip().replace(",", "")
|
| 51 |
+
q = row["question"] + "\nSolve step by step, then write the final number after '####'."
|
| 52 |
+
draft, final = pipe.generate(q, gen_len=gen_len, steps=steps, block_len=block_len, use_bart=use_bart)
|
| 53 |
+
ok_draft += extract_answer(draft) == gold
|
| 54 |
+
ok_final += extract_answer(final) == gold
|
| 55 |
+
if (i + 1) % 10 == 0:
|
| 56 |
+
print(f" {i+1}/{n} draft acc {ok_draft/(i+1):.3f} | refined acc {ok_final/(i+1):.3f}")
|
| 57 |
+
return {"n": n, "draft_acc": ok_draft / n, "refined_acc": ok_final / n}
|
dqwen/train.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loading, LoRA setup, stage-1 (diffusion adaptation) and stage-2 (BART fusion) training loops."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
from dataclasses import dataclass, field
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from .core import (DiffusionLM, diffusion_generate, find_text_model, load_rev_gates, masked_diffusion_loss,
|
| 13 |
+
patch_bidirectional, rev_gate_state)
|
| 14 |
+
from .data import batches
|
| 15 |
+
|
| 16 |
+
LORA_TARGETS = ["q_proj", "k_proj", "v_proj", "o_proj", # full-attention layers
|
| 17 |
+
"in_proj_qkv", "in_proj_z", "out_proj", # Gated-DeltaNet layers
|
| 18 |
+
"gate_proj", "up_proj", "down_proj"] # MLP
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Stage1Cfg:
|
| 23 |
+
max_steps: int = 300
|
| 24 |
+
batch_size: int = 2 # micro batch (sequences of max_len tokens)
|
| 25 |
+
grad_accum: int = 8
|
| 26 |
+
lr: float = 2e-4 # LoRA
|
| 27 |
+
gate_lr: float = 2e-3 # DeltaNet reverse gates
|
| 28 |
+
mask_lr: float = 1e-3 # [MASK] embedding
|
| 29 |
+
warmup: int = 20
|
| 30 |
+
min_lr_ratio: float = 0.1
|
| 31 |
+
weight_decay: float = 0.0
|
| 32 |
+
clip: float = 1.0
|
| 33 |
+
t_min: float = 0.05
|
| 34 |
+
weighting: str = "elbo" # or "uniform"
|
| 35 |
+
log_every: int = 5
|
| 36 |
+
sample_every: int = 100
|
| 37 |
+
save_every: int = 100
|
| 38 |
+
save_dir: str = "/content/diffusion_qwen_bart"
|
| 39 |
+
seed: int = 0
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load_dlm(model_id: str = "Qwen/Qwen3.5-9B", device: str = "cuda", dtype=torch.bfloat16,
|
| 43 |
+
load_in_4bit: bool = False, lora_r: int = 32, lora_alpha: int = 64, lora_dropout: float = 0.0,
|
| 44 |
+
mask_token: str = "<|fim_pad|>", grad_ckpt: bool = True, deltanet_bidirectional: bool = True,
|
| 45 |
+
lm: Optional[torch.nn.Module] = None, tok=None):
|
| 46 |
+
"""Returns (dlm, tok). `lm`/`tok` can be passed in for tests (skips from_pretrained)."""
|
| 47 |
+
from peft import LoraConfig, get_peft_model
|
| 48 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 49 |
+
|
| 50 |
+
if tok is None:
|
| 51 |
+
tok = AutoTokenizer.from_pretrained(model_id)
|
| 52 |
+
if lm is None:
|
| 53 |
+
kw = dict(dtype=dtype, attn_implementation="sdpa")
|
| 54 |
+
if load_in_4bit:
|
| 55 |
+
from transformers import BitsAndBytesConfig
|
| 56 |
+
kw["quantization_config"] = BitsAndBytesConfig(
|
| 57 |
+
load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True,
|
| 58 |
+
bnb_4bit_compute_dtype=dtype)
|
| 59 |
+
kw["device_map"] = {"": 0} if device == "cuda" else None
|
| 60 |
+
lm = AutoModelForCausalLM.from_pretrained(model_id, **kw)
|
| 61 |
+
lm.config.use_cache = False
|
| 62 |
+
|
| 63 |
+
stats = patch_bidirectional(lm, deltanet_bidirectional)
|
| 64 |
+
print("[patch]", stats)
|
| 65 |
+
if grad_ckpt:
|
| 66 |
+
lm.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
| 67 |
+
|
| 68 |
+
cfg = LoraConfig(r=lora_r, lora_alpha=lora_alpha, lora_dropout=lora_dropout, target_modules=LORA_TARGETS,
|
| 69 |
+
bias="none")
|
| 70 |
+
lm = get_peft_model(lm, cfg)
|
| 71 |
+
# PEFT freezes non-LoRA weights. Reactivate and verify reverse gates.
|
| 72 |
+
for n, p in lm.named_parameters():
|
| 73 |
+
if n.endswith("rev_gate"):
|
| 74 |
+
p.requires_grad_(True)
|
| 75 |
+
if p.device.type != torch.device(device).type:
|
| 76 |
+
raise RuntimeError(f"{n} is on {p.device}, expected {device}; rerun model loading with patched core.py")
|
| 77 |
+
|
| 78 |
+
mask_id = tok.convert_tokens_to_ids(mask_token)
|
| 79 |
+
dlm = DiffusionLM(lm, mask_id=mask_id)
|
| 80 |
+
dlm.mask_embed.requires_grad_(True)
|
| 81 |
+
if device == "cuda":
|
| 82 |
+
dlm.mask_embed.data = dlm.mask_embed.data.cuda()
|
| 83 |
+
n_tr = sum(p.numel() for p in dlm.parameters() if p.requires_grad)
|
| 84 |
+
print(f"[model] trainable params: {n_tr/1e6:.1f}M | mask token {mask_token!r} id={mask_id}")
|
| 85 |
+
return dlm, tok
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _groups(dlm, cfg: Stage1Cfg):
|
| 89 |
+
lora, gates, other = [], [], []
|
| 90 |
+
for n, p in dlm.named_parameters():
|
| 91 |
+
if not p.requires_grad:
|
| 92 |
+
continue
|
| 93 |
+
(gates if n.endswith("rev_gate") else other if n.endswith("mask_embed") else lora).append(p)
|
| 94 |
+
return [
|
| 95 |
+
{"params": lora, "lr": cfg.lr, "base": cfg.lr},
|
| 96 |
+
{"params": gates, "lr": cfg.gate_lr, "base": cfg.gate_lr},
|
| 97 |
+
{"params": other, "lr": cfg.mask_lr, "base": cfg.mask_lr},
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _lr_at(step, cfg: Stage1Cfg):
|
| 102 |
+
if step < cfg.warmup:
|
| 103 |
+
return (step + 1) / cfg.warmup
|
| 104 |
+
pr = (step - cfg.warmup) / max(1, cfg.max_steps - cfg.warmup)
|
| 105 |
+
return cfg.min_lr_ratio + (1 - cfg.min_lr_ratio) * 0.5 * (1 + math.cos(math.pi * min(1.0, pr)))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def save_checkpoint(dlm: DiffusionLM, tok, save_dir: str, step: int, fusion=None):
|
| 109 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 110 |
+
dlm.lm.save_pretrained(os.path.join(save_dir, "qwen_lora"))
|
| 111 |
+
tok.save_pretrained(os.path.join(save_dir, "qwen_lora"))
|
| 112 |
+
torch.save({"mask_embed": dlm.mask_embed.detach().cpu(), "rev_gates": rev_gate_state(dlm.lm),
|
| 113 |
+
"mask_id": dlm.mask_id, "step": step}, os.path.join(save_dir, "extras.pt"))
|
| 114 |
+
if fusion is not None:
|
| 115 |
+
torch.save(fusion.state_dict(), os.path.join(save_dir, "fusion.pt"))
|
| 116 |
+
print(f"[ckpt] saved step {step} -> {save_dir}")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def load_checkpoint(dlm: DiffusionLM, save_dir: str, fusion=None):
|
| 120 |
+
from peft import set_peft_model_state_dict
|
| 121 |
+
from safetensors.torch import load_file
|
| 122 |
+
sd = load_file(os.path.join(save_dir, "qwen_lora", "adapter_model.safetensors"))
|
| 123 |
+
set_peft_model_state_dict(dlm.lm, sd)
|
| 124 |
+
ex = torch.load(os.path.join(save_dir, "extras.pt"), map_location="cpu")
|
| 125 |
+
dlm.mask_embed.data.copy_(ex["mask_embed"].to(dlm.mask_embed.device))
|
| 126 |
+
missing = load_rev_gates(dlm.lm, ex["rev_gates"])
|
| 127 |
+
if missing:
|
| 128 |
+
print("[ckpt] WARNING rev_gate keys not matched:", missing[:3])
|
| 129 |
+
fp = os.path.join(save_dir, "fusion.pt")
|
| 130 |
+
if fusion is not None and os.path.exists(fp):
|
| 131 |
+
fusion.load_state_dict(torch.load(fp, map_location="cpu"))
|
| 132 |
+
return ex.get("step", 0)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@torch.no_grad()
|
| 136 |
+
def quick_sample(dlm, tok, prompt: str, eos_id: int, device, gen_len=128, steps=48, block_len=32, chat_kwargs=None):
|
| 137 |
+
was = dlm.training
|
| 138 |
+
dlm.eval()
|
| 139 |
+
text = tok.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False,
|
| 140 |
+
add_generation_prompt=True, **(chat_kwargs or {"enable_thinking": False}))
|
| 141 |
+
p = torch.tensor([tok(text, add_special_tokens=False)["input_ids"]], device=device)
|
| 142 |
+
out = diffusion_generate(dlm, p, gen_len=gen_len, steps=steps, block_len=block_len, eos_id=eos_id)
|
| 143 |
+
ids = out[0].tolist()
|
| 144 |
+
if eos_id in ids:
|
| 145 |
+
ids = ids[:ids.index(eos_id)]
|
| 146 |
+
dlm.train(was)
|
| 147 |
+
return tok.decode(ids, skip_special_tokens=True)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def train_stage1(dlm: DiffusionLM, tok, ds: dict, cfg: Stage1Cfg, device="cuda", sample_prompts=None,
|
| 151 |
+
eos_token="<|im_end|>", resume: bool = False):
|
| 152 |
+
torch.manual_seed(cfg.seed)
|
| 153 |
+
eos_id = tok.convert_tokens_to_ids(eos_token)
|
| 154 |
+
opt = torch.optim.AdamW(_groups(dlm, cfg), weight_decay=cfg.weight_decay, betas=(0.9, 0.99))
|
| 155 |
+
start = 0
|
| 156 |
+
if resume and os.path.exists(os.path.join(cfg.save_dir, "extras.pt")):
|
| 157 |
+
start = load_checkpoint(dlm, cfg.save_dir)
|
| 158 |
+
print(f"[resume] from step {start}")
|
| 159 |
+
dlm.train()
|
| 160 |
+
it = batches(ds, cfg.batch_size, device, seed=cfg.seed + start)
|
| 161 |
+
t0, tok_count, ema = time.time(), 0, None
|
| 162 |
+
for step in range(start, cfg.max_steps):
|
| 163 |
+
for g in opt.param_groups:
|
| 164 |
+
g["lr"] = g["base"] * _lr_at(step, cfg)
|
| 165 |
+
tot = 0.0
|
| 166 |
+
for _ in range(cfg.grad_accum):
|
| 167 |
+
ids, resp, _plen = next(it)
|
| 168 |
+
loss, info = masked_diffusion_loss(dlm, ids, resp, t_min=cfg.t_min, weighting=cfg.weighting)
|
| 169 |
+
(loss / cfg.grad_accum).backward()
|
| 170 |
+
tot += float(loss.detach()) / cfg.grad_accum
|
| 171 |
+
tok_count += ids.numel()
|
| 172 |
+
gn = torch.nn.utils.clip_grad_norm_([p for g in opt.param_groups for p in g["params"]], cfg.clip)
|
| 173 |
+
opt.step(); opt.zero_grad(set_to_none=True)
|
| 174 |
+
ema = tot if ema is None else 0.9 * ema + 0.1 * tot
|
| 175 |
+
if (step + 1) % cfg.log_every == 0 or step == start:
|
| 176 |
+
dt = time.time() - t0
|
| 177 |
+
print(f"step {step+1}/{cfg.max_steps} loss {tot:.4f} (ema {ema:.4f}) gnorm {float(gn):.2f} "
|
| 178 |
+
f"lr {opt.param_groups[0]['lr']:.2e} | {tok_count/dt:,.0f} tok/s")
|
| 179 |
+
if sample_prompts and (step + 1) % cfg.sample_every == 0:
|
| 180 |
+
for sp in sample_prompts[:2]:
|
| 181 |
+
print(f" [sample] {sp!r} -> {quick_sample(dlm, tok, sp, eos_id, device)!r}")
|
| 182 |
+
if (step + 1) % cfg.save_every == 0 or step + 1 == cfg.max_steps:
|
| 183 |
+
save_checkpoint(dlm, tok, cfg.save_dir, step + 1)
|
| 184 |
+
return dlm
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ------------------------------------------------------------------------------------------
|
| 188 |
+
@dataclass
|
| 189 |
+
class Stage2Cfg:
|
| 190 |
+
max_steps: int = 200
|
| 191 |
+
batch_size: int = 2
|
| 192 |
+
grad_accum: int = 4
|
| 193 |
+
lr: float = 5e-4
|
| 194 |
+
gate_lr: float = 3e-3
|
| 195 |
+
warmup: int = 10
|
| 196 |
+
clip: float = 1.0
|
| 197 |
+
log_every: int = 5
|
| 198 |
+
save_every: int = 100
|
| 199 |
+
save_dir: str = "/content/diffusion_qwen_bart"
|
| 200 |
+
seed: int = 0
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def train_stage2_fusion(dlm: DiffusionLM, tok, bart, btok, fusion, ds: dict, cfg: Stage2Cfg, device="cuda",
|
| 204 |
+
eos_token="<|im_end|>"):
|
| 205 |
+
"""Only the fusion module is trained; Qwen (LoRA) and BART are frozen."""
|
| 206 |
+
from .bart_fusion import refine_loss
|
| 207 |
+
torch.manual_seed(cfg.seed)
|
| 208 |
+
eos_id = tok.convert_tokens_to_ids(eos_token)
|
| 209 |
+
for p in dlm.parameters():
|
| 210 |
+
p.requires_grad_(False)
|
| 211 |
+
for p in bart.parameters():
|
| 212 |
+
p.requires_grad_(False)
|
| 213 |
+
bart.eval()
|
| 214 |
+
fusion.train()
|
| 215 |
+
for p in fusion.parameters():
|
| 216 |
+
p.requires_grad_(True)
|
| 217 |
+
gate = [fusion.gate]
|
| 218 |
+
rest = [p for n, p in fusion.named_parameters() if n != "gate"]
|
| 219 |
+
opt = torch.optim.AdamW([{"params": rest, "lr": cfg.lr, "base": cfg.lr},
|
| 220 |
+
{"params": gate, "lr": cfg.gate_lr, "base": cfg.gate_lr}], weight_decay=0.0)
|
| 221 |
+
dlm.train()
|
| 222 |
+
it = batches(ds, cfg.batch_size, device, seed=cfg.seed)
|
| 223 |
+
t0 = time.time()
|
| 224 |
+
for step in range(cfg.max_steps):
|
| 225 |
+
sc = min(1.0, (step + 1) / cfg.warmup) * (0.5 * (1 + math.cos(math.pi * step / cfg.max_steps)) * 0.9 + 0.1)
|
| 226 |
+
for g in opt.param_groups:
|
| 227 |
+
g["lr"] = g["base"] * sc
|
| 228 |
+
tot = 0.0
|
| 229 |
+
for _ in range(cfg.grad_accum):
|
| 230 |
+
ids, resp, plen = next(it)
|
| 231 |
+
loss, info = refine_loss(dlm, fusion, bart, btok, tok, ids, resp, plen, eos_id, device)
|
| 232 |
+
(loss / cfg.grad_accum).backward()
|
| 233 |
+
tot += float(loss.detach()) / cfg.grad_accum
|
| 234 |
+
gn = torch.nn.utils.clip_grad_norm_(fusion.parameters(), cfg.clip)
|
| 235 |
+
opt.step(); opt.zero_grad(set_to_none=True)
|
| 236 |
+
if (step + 1) % cfg.log_every == 0 or step == 0:
|
| 237 |
+
print(f"[fusion] step {step+1}/{cfg.max_steps} loss {tot:.4f} gnorm {float(gn):.2f} "
|
| 238 |
+
f"gate {float(torch.tanh(fusion.gate)):+.4f} | {time.time()-t0:.0f}s")
|
| 239 |
+
if (step + 1) % cfg.save_every == 0 or step + 1 == cfg.max_steps:
|
| 240 |
+
os.makedirs(cfg.save_dir, exist_ok=True)
|
| 241 |
+
torch.save(fusion.state_dict(), os.path.join(cfg.save_dir, "fusion.pt"))
|
| 242 |
+
return fusion
|