#!/usr/bin/env python3 """dhara-chat — ZeroGPU demo of the dhara-250M tri-mode model. "Denoiser" terminal aesthetic. Chat lets you pick the decoding mode (AR types left-to-right; diffusion visibly unmasks ▓▒░ blocks). The tri-mode compare tab streams all three modes so you can watch AR type, block-diffusion denoise, and self-speculation jump in accepted spans — each with tokens/sec. ZeroGPU rules this file follows (per HF docs): * the model is placed on cuda at ROOT MODULE level — required; lazy-loading or moving to cuda inside @spaces.GPU is discouraged (a CUDA emulation mode makes the module-level placement work before a real GPU is attached). * every entrypoint that touches the GPU is wrapped in @spaces.GPU — here the two streaming Gradio handlers. The GPU is attached for the call and released after. * duration=N raises the 60s default; keep it tight, since shorter durations get better queue priority. Hardware ("ZeroGPU") is selected in Space *settings*, not in README metadata. Needs torch>=2.8, gradio 4+. torch.compile is NOT supported on ZeroGPU. """ import spaces # noqa: F401 — import before torch so CUDA emulation is in place import os, re, time, threading import torch import torch.nn.functional as F import gradio as gr from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer MODEL_ID = os.environ.get("DHARA_MODEL", "codelion/dhara-250m") TOKEN = os.environ.get("HF_TOKEN") DEVICE = "cuda" tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True, token=TOKEN) # bf16 on GPU. (The old CPU build used fp32 + torch.quantization.quantize_dynamic # INT8 — that path is CPU-only and must not be used here.) model = AutoModelForCausalLM.from_pretrained( MODEL_ID, trust_remote_code=True, dtype=torch.bfloat16, token=TOKEN).eval().to(DEVICE) IM_END = tok.convert_tokens_to_ids("<|im_end|>") MASK = int(model.config.mask_token_id) GEN = dict(do_sample=True, temperature=0.7, top_p=0.9, repetition_penalty=1.2, no_repeat_ngram_size=3) GEN_GREEDY = dict(do_sample=False, repetition_penalty=1.3, no_repeat_ngram_size=3) REP = 1.3 # repetition penalty for diffusion/self-spec unmasking (prevents "capital capital" collapse) def _msg_text(c): # gradio 6 Chatbot stores content as a list of parts; flatten to plain text for the template if isinstance(c, list): return "".join((p.get("text") or "") if isinstance(p, dict) else str(p) for p in c) return c if isinstance(c, str) else str(c) def _enc(messages, max_tok=1024): msgs = [{"role": m.get("role", "user"), "content": _msg_text(m.get("content", ""))} for m in messages] while True: # sliding window: drop oldest turns until the prompt fits the 1k budget p = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True) e = tok(p, return_tensors="pt", add_special_tokens=False) if e.input_ids.shape[1] <= max_tok or len(msgs) <= 1: return e.input_ids, e.attention_mask msgs = msgs[1:] def _ntok(text): return len(tok(text, add_special_tokens=False).input_ids) def _block_mask(S, bl, dev, dt): idx = torch.arange(S, device=dev) allowed = (idx // bl).unsqueeze(0) <= (idx // bl).unsqueeze(1) return torch.zeros((S, S), device=dev, dtype=dt).masked_fill(~allowed, float("-inf"))[None, None] def _rep_pen(logits, seen, penalty=REP): if penalty == 1.0 or seen.numel() == 0: return logits u = torch.unique(seen) s = logits[:, u] logits[:, u] = torch.where(s > 0, s / penalty, s * penalty) return logits def _clip(text): """Trim a trailing incomplete sentence so responses never end mid-word.""" t = text.rstrip() ends = [m.start() for m in re.finditer(r"[.!?](?=\s|$)", t)] if ends and ends[-1] >= 16: t = t[:ends[-1] + 1] return re.sub(r"\s+\d+\.$", "", t).rstrip() def _render(row, prompt_len): out = [] for t in row[prompt_len:]: if t == MASK: out.append("▒") elif t == IM_END: break else: out.append(tok.decode([t])) return "".join(out) def _ar_stream(ids, am, max_new, gen=GEN): streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True) threading.Thread(target=model.generate, kwargs=dict( input_ids=ids, attention_mask=am, max_new_tokens=max_new, eos_token_id=IM_END, pad_token_id=IM_END, streamer=streamer, **gen)).start() out = "" for tk_ in streamer: out += tk_ yield out @torch.no_grad() def _diffusion_stream(ids, block_len=32, threshold=0.5, max_new=64): dev = ids.device; dt = next(model.parameters()).dtype cur = ids; gen = 0 while gen < max_new: seq = torch.cat([cur, torch.full((1, block_len), MASK, device=dev)], 1); S = seq.shape[1] bias = _block_mask(S, block_len, dev, dt) for _ in range(block_len): mp = (seq[0] == MASK).nonzero(as_tuple=True)[0] if mp.numel() == 0: break lg = model(input_ids=seq, trimode_bias=bias).logits[0].float() lgm = _rep_pen(lg[mp], seq[0][seq[0] != MASK]) conf, pred = F.softmax(lgm, -1).max(-1) take = conf >= threshold if take.sum() == 0: take[conf.argmax()] = True seq[0, mp[take]] = pred[take] yield _render(seq[0].tolist(), ids.shape[1]) cur = seq; gen += block_len if (cur[0, -block_len:] == IM_END).any(): break @torch.no_grad() def _selfspec_stream(ids, k=8, block_len=32, max_new=48): dev = ids.device; dt = next(model.parameters()).dtype cur = ids; gen = 0 while gen < max_new: n = cur.shape[1] seq = torch.cat([cur, torch.full((1, k), MASK, device=dev)], 1); S = seq.shape[1] seen = cur[0] dl = model(input_ids=seq, trimode_bias=_block_mask(S, block_len, dev, dt)).logits[0].float() draft = _rep_pen(dl[n:n + k], seen).argmax(-1) cand = torch.cat([cur, draft.unsqueeze(0)], 1) al = model(input_ids=cand).logits[0].float() ar_pred = _rep_pen(al[n - 1:n + k - 1], seen).argmax(-1) match = (draft == ar_pred) m = int((~match).float().argmax().item()) if (~match).any() else k new = torch.cat([draft[:m], ar_pred[m:m + 1]]) if m < k else torch.cat([draft, al[n + k - 1:n + k].argmax(-1)]) cur = torch.cat([cur, new.unsqueeze(0)], 1); gen += new.numel() yield tok.decode(cur[0, ids.shape[1]:], skip_special_tokens=True) if IM_END in new.tolist(): break def _pane(label, speed, text): sp = f" {speed}" if speed else "" return f"