Ouzhang's picture
Add files using upload-large-folder tool
13c5606 verified
Raw
History Blame Contribute Delete
34.1 kB
import argparse
import html
import json
import re
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
DEFAULT_MODEL_PATH = "Zigeng/DMax-Math-16B"
DEFAULT_PROMPT = "Solve 37 * 48 and explain the intermediate reasoning briefly."
STAGE_SIDEBAR_WIDTH = 320
STAGE_WIDTH = 720
STAGE_MIN_HEIGHT = 620
STAGE_FINAL_HEIGHT_VH = 72
STAGE_FINAL_MAX_HEIGHT = 860
TOKEN_GRID_MIN_HEIGHT = 600
FINAL_MARKDOWN_BASE_FONT_SIZE = 20
FINAL_MARKDOWN_MIN_FONT_SIZE = 11
PLAYBACK_INTERVAL_MS = 180
def parse_args():
parser = argparse.ArgumentParser(
description="Visualize the full step-by-step decoding process of the diffusion LM."
)
parser.add_argument("--model-path", default=DEFAULT_MODEL_PATH)
parser.add_argument("--device", default="cuda:0")
parser.add_argument("--prompt", default=None)
parser.add_argument("--gsm8k-index", type=int, default=1)
parser.add_argument("--gen-length", type=int, default=512)
parser.add_argument("--block-length", type=int, default=32)
parser.add_argument("--steps", type=int, default=32)
parser.add_argument("--threshold", type=float, default=0.0)
parser.add_argument("--output", default="dllm_demo.html")
return parser.parse_args()
def load_prompt(args):
if args.prompt is not None:
return args.prompt
try:
from datasets import load_dataset
ds = load_dataset("openai/gsm8k", "main", split="test")
return ds[args.gsm8k_index]["question"] + "\nLet's think step by step\n"
except Exception:
return DEFAULT_PROMPT
def tokenize_prompt(tokenizer, prompt):
if hasattr(tokenizer, "apply_chat_template"):
return tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}],
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
)
return tokenizer(prompt, return_tensors="pt").input_ids
def format_token_label(tokenizer, token_id, mask_id, eos_id):
if token_id == mask_id:
return "[MASK]"
if eos_id is not None and token_id == eos_id:
return "[EOS]"
text = tokenizer.decode(
[token_id],
skip_special_tokens=False,
clean_up_tokenization_spaces=False,
)
if not text:
text = tokenizer.convert_ids_to_tokens(token_id)
if text is None:
text = str(token_id)
text = (
text.replace("\n", "↵")
.replace("\t", "⇥")
.replace("\r", "␍")
.replace(" ", "·")
)
if text == "":
text = "∅"
if len(text) > 20:
text = text[:17] + "..."
return text
def collect_token_labels(tokenizer, demo_trace):
used_ids = set(demo_trace["prompt_token_ids"])
used_ids.update(demo_trace["generated_token_ids"])
used_ids.update(demo_trace["final_token_ids"])
for frame in demo_trace["frames"]:
used_ids.update(frame.get("visible_generated_ids", []))
used_ids.update(frame.get("pre_visible_ids", []))
used_ids.update(frame.get("post_visible_ids", []))
used_ids.update(frame.get("top1_token_ids", []))
used_ids.update(frame.get("block_top1_token_ids", []))
mask_id = demo_trace["mask_id"]
eos_id = demo_trace["eos_id"]
token_labels = {}
for token_id in sorted(used_ids):
token_labels[str(token_id)] = format_token_label(
tokenizer, token_id, mask_id, eos_id
)
return token_labels
def enrich_demo_trace_for_render(demo_trace):
prompt_length = demo_trace["prompt_length"]
original_frames = demo_trace["frames"]
if not original_frames:
return demo_trace
enriched_frames = []
for frame in original_frames:
generated_before_ids = frame["pre_visible_ids"][prompt_length:]
generated_after_ids = frame["post_visible_ids"][prompt_length:]
current_block_abs_start = max(prompt_length, frame["block_start"])
current_block_generated_start = max(0, current_block_abs_start - prompt_length)
prompt_overlap = max(0, prompt_length - frame["block_start"])
visible_generated_absolute_positions = list(
range(prompt_length, frame["window_end"])
)
frame["generated_before_ids"] = generated_before_ids
frame["generated_after_ids"] = generated_after_ids
frame["visible_generated_ids"] = generated_after_ids
frame["visible_generated_absolute_positions"] = visible_generated_absolute_positions
frame["current_block_absolute_start"] = current_block_abs_start
frame["current_block_generated_start"] = current_block_generated_start
frame["block_top1_confidence"] = frame["top1_confidence"][prompt_overlap:]
frame["block_top1_token_ids"] = frame["top1_token_ids"][prompt_overlap:]
frame["block_input_confidence"] = frame["input_confidence"][prompt_overlap:]
frame["block_mask_index_before"] = frame["mask_index_before"][prompt_overlap:]
frame["block_token_index_before"] = frame["token_index_before"][prompt_overlap:]
frame["block_active_mask"] = frame["active_block_mask"][prompt_overlap:]
frame["block_absolute_positions"] = list(
range(current_block_abs_start, frame["block_end"])
)
frame["block_prompt_overlap"] = prompt_overlap
frame["block_decoded_positions"] = [
pos - prompt_overlap
for pos in frame["decoded_positions"]
if pos >= prompt_overlap
]
frame["decoded_absolute_positions"] = [
current_block_abs_start + pos for pos in frame["block_decoded_positions"]
]
frame["state_mode"] = "after_step"
enriched_frames.append(frame)
first_frame = enriched_frames[0]
initial_frame = {
"frame_id": -1,
"block_id": first_frame["block_id"],
"absolute_block_id": first_frame["absolute_block_id"],
"step_id": -1,
"window_end": first_frame["window_end"],
"block_start": first_frame["block_start"],
"block_end": first_frame["block_end"],
"nfe": 0,
"visible_generated_ids": first_frame["generated_before_ids"],
"visible_generated_absolute_positions": list(
range(prompt_length, first_frame["window_end"])
),
"current_block_absolute_start": first_frame["current_block_absolute_start"],
"current_block_generated_start": first_frame["current_block_generated_start"],
"block_top1_confidence": [],
"block_top1_token_ids": [],
"block_input_confidence": first_frame["block_input_confidence"],
"block_mask_index_before": first_frame["block_mask_index_before"],
"block_token_index_before": first_frame["block_token_index_before"],
"block_active_mask": first_frame["block_active_mask"],
"block_absolute_positions": first_frame["block_absolute_positions"],
"block_prompt_overlap": first_frame["block_prompt_overlap"],
"block_decoded_positions": [],
"decoded_absolute_positions": [],
"same_as_previous": False,
"all_confident": False,
"converged": False,
"convergence_reason": None,
"state_mode": "before_first_step",
}
demo_trace["frames"] = [initial_frame]
for idx, frame in enumerate(enriched_frames, start=1):
frame["frame_id"] = idx
demo_trace["frames"].append(frame)
return demo_trace
def markdown_to_html(text):
escaped = html.escape(text.replace("\r\n", "\n").replace("\r", "\n"))
code_blocks = []
def stash_code(match):
code = match.group(1).strip("\n")
placeholder = f"@@CODEBLOCK{len(code_blocks)}@@"
code_blocks.append(f"<pre><code>{code}</code></pre>")
return placeholder
escaped = re.sub(r"```(?:[^\n`]*)\n(.*?)```", stash_code, escaped, flags=re.S)
def format_inline(content):
content = re.sub(r"`([^`]+)`", r"<code>\1</code>", content)
content = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", content)
content = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", content)
return content
parts = []
paragraph_lines = []
in_ul = False
in_ol = False
def close_lists():
nonlocal in_ul, in_ol
if in_ul:
parts.append("</ul>")
in_ul = False
if in_ol:
parts.append("</ol>")
in_ol = False
def flush_paragraph():
nonlocal paragraph_lines
if paragraph_lines:
merged_parts = []
non_empty_lines = [line for line in paragraph_lines if line.strip()]
for idx, line in enumerate(non_empty_lines):
hard_break = line.endswith(" ") or line.endswith("\\")
content = line[:-1] if line.endswith("\\") else line
merged_parts.append(format_inline(content.strip()))
if idx < len(non_empty_lines) - 1:
merged_parts.append("<br />" if hard_break else " ")
merged = "".join(merged_parts)
if merged:
parts.append(f"<p>{merged}</p>")
paragraph_lines = []
for raw_line in escaped.split("\n"):
line = raw_line
stripped = line.strip()
if not stripped:
flush_paragraph()
close_lists()
continue
heading = re.match(r"^(#{1,6})\s+(.*)$", stripped)
unordered = re.match(r"^[-*+]\s+(.*)$", stripped)
ordered = re.match(r"^(\d+)\.\s+(.*)$", stripped)
code_block = re.fullmatch(r"@@CODEBLOCK(\d+)@@", stripped)
if code_block:
flush_paragraph()
close_lists()
parts.append(stripped)
elif heading:
flush_paragraph()
close_lists()
level = len(heading.group(1))
parts.append(f"<h{level}>{format_inline(heading.group(2))}</h{level}>")
elif unordered:
flush_paragraph()
if in_ol:
parts.append("</ol>")
in_ol = False
if not in_ul:
parts.append("<ul>")
in_ul = True
parts.append(f"<li>{format_inline(unordered.group(1))}</li>")
elif ordered:
flush_paragraph()
if in_ul:
parts.append("</ul>")
in_ul = False
if not in_ol:
parts.append(f'<ol start="{ordered.group(1)}">')
in_ol = True
parts.append(f"<li>{format_inline(ordered.group(2))}</li>")
else:
close_lists()
paragraph_lines.append(line)
flush_paragraph()
close_lists()
rendered = "\n".join(parts)
for idx, block in enumerate(code_blocks):
rendered = rendered.replace(f"@@CODEBLOCK{idx}@@", block)
return rendered
def build_demo_payload(prompt, generated_answer, demo_trace, nfe, token_labels):
generated_token_count = len(demo_trace["generated_token_ids"])
tpf = generated_token_count / nfe if nfe else 0.0
return {
"title": "Diffusion Decoding Demo",
"prompt": prompt,
"generated_answer": generated_answer,
"nfe": nfe,
"tpf": tpf,
"prompt_length": demo_trace["prompt_length"],
"block_length": demo_trace["block_length"],
"steps": demo_trace["steps"],
"threshold": demo_trace["threshold"],
"frames": demo_trace["frames"],
"blocks": demo_trace["blocks"],
"token_labels": token_labels,
"mask_id": demo_trace["mask_id"],
"eos_id": demo_trace["eos_id"],
"generated_token_ids": demo_trace["generated_token_ids"],
"generated_html": markdown_to_html(generated_answer),
}
def render_html(payload):
payload_json = json.dumps(payload, ensure_ascii=False)
html = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diffusion Decoding Demo</title>
<style>
:root {{
--bg: #f5efe5;
--bg-soft: #f9f5ee;
--panel: rgba(255, 252, 247, 0.82);
--ink: #1d1b18;
--muted: #6d665d;
--line: rgba(68, 55, 39, 0.12);
--accent: #b85c38;
--accent-2: #0f766e;
--accent-3: #6d4aa2;
--prompt: #d9e6f2;
--active-token: #d7efe7;
--active-mask: #f8e3b3;
--decoded: #ffd7c2;
--stable: #dde8ff;
--shadow: 0 24px 60px rgba(102, 73, 31, 0.12);
--radius: 22px;
}}
* {{
box-sizing: border-box;
}}
body {{
margin: 0;
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(184, 92, 56, 0.10), transparent 28%),
radial-gradient(circle at top right, rgba(15, 118, 110, 0.10), transparent 24%),
linear-gradient(180deg, #fcf8f2 0%, var(--bg) 100%);
}}
.page {{
max-width: 1440px;
margin: 0 auto;
padding: 28px 20px 40px;
}}
.hero {{
display: grid;
grid-template-columns: 1.35fr 0.95fr;
gap: 20px;
margin-bottom: 20px;
}}
.panel {{
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}}
.hero-main {{
padding: 24px 26px;
position: relative;
overflow: hidden;
}}
.hero-main::after {{
content: "";
position: absolute;
inset: auto -10% -45% auto;
width: 280px;
height: 280px;
border-radius: 999px;
background: radial-gradient(circle, rgba(184, 92, 56, 0.12), transparent 68%);
pointer-events: none;
}}
.eyebrow {{
margin: 0 0 8px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 12px;
color: var(--accent);
font-weight: 700;
}}
h1 {{
margin: 0;
font-size: clamp(32px, 5vw, 56px);
line-height: 0.95;
letter-spacing: -0.03em;
}}
.hero-copy {{
margin-top: 14px;
max-width: 60ch;
color: var(--muted);
font-size: 16px;
line-height: 1.6;
}}
.summary-grid {{
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
padding: 20px;
}}
.metric {{
padding: 18px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.64);
border: 1px solid var(--line);
}}
.metric-label {{
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 8px;
}}
.metric-value {{
font-size: 28px;
line-height: 1;
font-weight: 700;
}}
.metric-sub {{
margin-top: 8px;
font-size: 13px;
color: var(--muted);
}}
.content {{
display: grid;
grid-template-columns: __STAGE_SIDEBAR_WIDTH__px __STAGE_WIDTH__px;
justify-content: center;
gap: 20px;
}}
.sidebar,
.workspace {{
padding: 20px;
}}
.section-title {{
margin: 0 0 12px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}}
.prompt-box,
.answer-box {{
padding: 16px 18px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.7);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: #2b2621;
}}
.answer-box {{
width: 100%;
height: 100%;
min-height: 0;
font-size: __FINAL_MARKDOWN_BASE_FONT_SIZE__px;
line-height: 1.08;
padding: 10px 12px;
overflow: hidden;
}}
.answer-box h1,
.answer-box h2,
.answer-box h3,
.answer-box h4 {{
margin: 0;
line-height: 1.02;
font-size: 1em;
font-weight: 700;
}}
.answer-box p,
.answer-box ul,
.answer-box ol,
.answer-box pre {{
margin: 0;
}}
.answer-box ul,
.answer-box ol {{
padding-left: 0.95em;
}}
.answer-box li {{
margin: 0;
}}
.answer-box code {{
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
background: rgba(31, 92, 87, 0.08);
padding: 0.03em 0.16em;
border-radius: 4px;
}}
.answer-box pre {{
overflow: hidden;
padding: 4px 6px;
border-radius: 6px;
background: rgba(29, 27, 24, 0.06);
}}
.answer-box pre code {{
background: transparent;
padding: 0;
}}
.answer-box > * + * {{
margin-top: 0 !important;
}}
.controls {{
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}}
button {{
border: 0;
border-radius: 999px;
padding: 10px 14px;
background: #1f5c57;
color: white;
cursor: pointer;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.02em;
}}
button.secondary {{
background: rgba(31, 92, 87, 0.12);
color: #1f5c57;
}}
input[type="range"] {{
width: 100%;
accent-color: var(--accent);
}}
.badge {{
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
background: rgba(184, 92, 56, 0.12);
color: var(--accent);
}}
.token-section {{
margin-top: 14px;
width: 100%;
}}
.token-section h3 {{
margin: 0 0 10px;
font-size: 16px;
}}
.stage-box {{
width: 100%;
min-height: __STAGE_MIN_HEIGHT__px;
max-width: __STAGE_WIDTH__px;
margin: 0 auto;
padding: 10px;
border-radius: 22px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.6);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
overflow: visible;
display: block;
}}
.stage-box.final-stage {{
height: __STAGE_FINAL_HEIGHT_VH__vh;
max-height: __STAGE_FINAL_MAX_HEIGHT__px;
overflow: hidden;
}}
.token-grid {{
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: flex-start;
align-content: flex-start;
width: 100%;
height: auto;
min-height: __TOKEN_GRID_MIN_HEIGHT__px;
overflow: visible;
}}
.token {{
position: relative;
min-width: 38px;
max-width: 96px;
padding: 6px 7px 5px;
border-radius: 12px;
border: 1px solid rgba(53, 44, 33, 0.12);
background: rgba(255, 255, 255, 0.88);
overflow: hidden;
box-shadow: 0 6px 14px rgba(61, 48, 31, 0.05);
}}
.token::before {{
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: calc(var(--conf, 0) * 100%);
background: linear-gradient(90deg, rgba(184, 92, 56, 0.14), rgba(15, 118, 110, 0.20));
pointer-events: none;
}}
.token.prompt {{ background: var(--prompt); }}
.token.mask {{ background: var(--active-mask); }}
.token.token-index {{ background: var(--stable); }}
.token.decoded {{ background: var(--decoded); }}
.token.refreshed {{ background: var(--active-token); }}
.token.committed {{ background: rgba(255, 255, 255, 0.96); }}
.token.current-block {{
outline: 2px solid rgba(109, 74, 162, 0.18);
outline-offset: 1px;
}}
.token-label {{
position: relative;
z-index: 1;
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 10px;
line-height: 1.15;
word-break: break-word;
white-space: pre-wrap;
}}
.legend {{
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}}
.legend span {{
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
}}
.swatch {{
width: 14px;
height: 14px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.08);
}}
.token-caption {{
margin: 6px 0 0;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}}
.legend[hidden],
.token-caption[hidden],
.token-grid[hidden],
.answer-box[hidden],
.stage-placeholder[hidden] {{
display: none !important;
}}
.stage-placeholder {{
width: 100%;
height: 100%;
padding: 12px 14px;
border-radius: 14px;
border: 1px dashed var(--line);
color: var(--muted);
background: rgba(255, 255, 255, 0.58);
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}}
@media (max-width: 1100px) {{
.hero,
.content {{
grid-template-columns: 1fr;
}}
}}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="panel hero-main">
<p class="eyebrow">Diffusion Language Model</p>
<h1>DMax Decoding Process,<br />made visible.</h1>
<div class="hero-copy">
Inspect the full block-wise diffusion process frame by frame.
</div>
</div>
<div class="panel summary-grid">
<div class="metric">
<div class="metric-label">TPF</div>
<div class="metric-value" id="metric-frames"></div>
<div class="metric-sub">Generated tokens / NFE</div>
</div>
<div class="metric">
<div class="metric-label">NFE</div>
<div class="metric-value" id="metric-nfe"></div>
<div class="metric-sub">Forward passes</div>
</div>
<div class="metric">
<div class="metric-label">Generated Tokens</div>
<div class="metric-value" id="metric-gen"></div>
<div class="metric-sub">Final visible answer length</div>
</div>
<div class="metric">
<div class="metric-label">Threshold</div>
<div class="metric-value" id="metric-threshold"></div>
<div class="metric-sub">Mask decode cutoff</div>
</div>
</div>
</section>
<section class="content">
<aside class="panel sidebar">
<h2 class="section-title">Prompt</h2>
<div class="prompt-box" id="prompt-box"></div>
</aside>
<main class="panel workspace">
<div class="controls">
<button id="play-btn">Play</button>
<button id="prev-btn" class="secondary">Prev</button>
<button id="next-btn" class="secondary">Next</button>
<span class="badge" id="convergence-badge">Running</span>
</div>
<input id="frame-slider" type="range" min="0" max="0" value="0" />
<section class="token-section">
<h3 id="stage-title">Visible generated tokens so far</h3>
<div class="stage-box">
<div class="token-grid" id="tokens-step"></div>
<div class="answer-box" id="answer-box" hidden></div>
<div class="stage-placeholder" id="stage-placeholder" hidden></div>
</div>
<p class="token-caption" id="token-caption">Previously decoded blocks stay visible and the raw token sequence grows over time. The final generated answer is rendered as markdown only after the full sequence finishes decoding.</p>
</section>
<div class="legend">
<span><i class="swatch" style="background: var(--active-mask);"></i>Mask before step</span>
<span><i class="swatch" style="background: var(--stable);"></i>Existing token index</span>
<span><i class="swatch" style="background: var(--active-token);"></i>Refreshed token</span>
<span><i class="swatch" style="background: var(--decoded);"></i>Decoded mask</span>
<span><i class="swatch" style="background: rgba(255, 255, 255, 0.96);"></i>Committed previous block</span>
</div>
</main>
</section>
</div>
<script>
const DATA = __PAYLOAD_JSON__;
const slider = document.getElementById("frame-slider");
const playBtn = document.getElementById("play-btn");
const prevBtn = document.getElementById("prev-btn");
const nextBtn = document.getElementById("next-btn");
const promptBox = document.getElementById("prompt-box");
const answerBox = document.getElementById("answer-box");
const metricFrames = document.getElementById("metric-frames");
const metricNfe = document.getElementById("metric-nfe");
const metricGen = document.getElementById("metric-gen");
const metricThreshold = document.getElementById("metric-threshold");
const convergenceBadge = document.getElementById("convergence-badge");
const stageTitle = document.getElementById("stage-title");
const stageBox = document.querySelector(".stage-box");
const tokensStep = document.getElementById("tokens-step");
const stagePlaceholder = document.getElementById("stage-placeholder");
const tokenCaption = document.getElementById("token-caption");
const legend = document.querySelector(".legend");
let currentFrame = 0;
let timer = null;
function tokenLabel(tokenId) {{
return DATA.token_labels[String(tokenId)] ?? String(tokenId);
}}
function formatReason(reason) {{
if (reason === "stable_tokens") return "Stable tokens";
if (reason === "high_confidence") return "High confidence";
return "Running";
}}
function buildTokenEl(tokenId, localIndex, frame) {{
const el = document.createElement("div");
const decodedSet = new Set(frame.decoded_absolute_positions || []);
const absolutePos = frame.visible_generated_absolute_positions[localIndex];
const inCurrentBlock =
absolutePos >= frame.current_block_absolute_start && absolutePos < frame.block_end;
const blockLocalIndex = inCurrentBlock ? absolutePos - frame.current_block_absolute_start : -1;
const inputConf =
inCurrentBlock && blockLocalIndex < frame.block_input_confidence.length
? Number(frame.block_input_confidence[blockLocalIndex] || 0)
: 0;
const top1Conf =
inCurrentBlock && blockLocalIndex < frame.block_top1_confidence.length
? Number(frame.block_top1_confidence[blockLocalIndex] || 0)
: 0;
let cls = [];
if (inCurrentBlock) {{
cls.push("current-block");
if (tokenId === DATA.mask_id) cls.push("mask");
if (decodedSet.has(absolutePos)) cls.push("decoded");
else if (
blockLocalIndex >= 0 &&
frame.block_token_index_before[blockLocalIndex]
) cls.push("refreshed");
else if (
blockLocalIndex >= 0 &&
frame.block_mask_index_before[blockLocalIndex]
) cls.push("mask");
else cls.push("token-index");
}} else {{
cls.push("committed");
}}
el.className = `token ${cls.join(" ")}`.trim();
el.style.setProperty("--conf", top1Conf);
el.title = `abs pos: ${absolutePos}\\ninput conf: ${inputConf.toFixed(3)}\\ntop1 conf: ${top1Conf.toFixed(3)}\\nid: ${tokenId}`;
const label = document.createElement("div");
label.className = "token-label";
label.textContent = tokenLabel(tokenId);
el.append(label);
return el;
}}
function renderTokenRow(container, ids, frame) {{
container.innerHTML = "";
ids.forEach((tokenId, idx) => {{
container.appendChild(buildTokenEl(tokenId, idx, frame));
}});
}}
function fitFinalMarkdown() {{
if (answerBox.hidden) return;
let fontSize = __FINAL_MARKDOWN_BASE_FONT_SIZE__;
let lineHeight = 1.08;
let guard = 0;
answerBox.style.fontSize = `${fontSize}px`;
answerBox.style.lineHeight = String(lineHeight);
while (answerBox.scrollHeight > answerBox.clientHeight && guard < 24) {{
fontSize = Math.max(__FINAL_MARKDOWN_MIN_FONT_SIZE__, fontSize - 0.5);
lineHeight = Math.max(0.95, lineHeight - 0.01);
answerBox.style.fontSize = `${fontSize}px`;
answerBox.style.lineHeight = String(lineHeight);
guard += 1;
}}
}}
function renderFrame(frameIndex) {{
currentFrame = frameIndex;
const frame = DATA.frames[frameIndex];
if (!frame) return;
const isFinalFrame = frameIndex === DATA.frames.length - 1;
const isInitialFrame = frame.state_mode === "before_first_step";
slider.value = String(frameIndex);
convergenceBadge.textContent = frame.converged
? formatReason(frame.convergence_reason)
: "Running";
if (isFinalFrame) {{
stageBox.classList.add("final-stage");
stageTitle.textContent = "Final markdown response";
tokensStep.hidden = true;
tokensStep.innerHTML = "";
stagePlaceholder.hidden = true;
stagePlaceholder.textContent = "";
answerBox.hidden = false;
answerBox.innerHTML = DATA.generated_html;
answerBox.style.fontSize = "";
answerBox.style.lineHeight = "";
tokenCaption.hidden = true;
legend.hidden = true;
requestAnimationFrame(fitFinalMarkdown);
}} else {{
stageBox.classList.remove("final-stage");
stageTitle.textContent = "Visible generated tokens so far";
answerBox.hidden = true;
answerBox.innerHTML = "";
tokensStep.hidden = false;
stagePlaceholder.hidden = true;
renderTokenRow(tokensStep, frame.visible_generated_ids, frame);
tokenCaption.hidden = false;
legend.hidden = false;
tokenCaption.textContent = "Previously decoded blocks stay visible and the raw token sequence grows over time. The final generated answer is rendered as markdown only after the full sequence finishes decoding.";
if (isInitialFrame && frame.visible_generated_ids.length === 0) {{
tokensStep.hidden = true;
stagePlaceholder.hidden = false;
stagePlaceholder.textContent = "This is the initial all-mask state before the first forward pass.";
}}
}}
}}
function stopPlayback() {{
if (timer !== null) {{
clearInterval(timer);
timer = null;
}}
playBtn.textContent = "Play";
}}
function togglePlayback() {{
if (timer !== null) {{
stopPlayback();
return;
}}
playBtn.textContent = "Pause";
timer = setInterval(() => {{
if (currentFrame >= DATA.frames.length - 1) {{
stopPlayback();
return;
}}
renderFrame(currentFrame + 1);
}}, __PLAYBACK_INTERVAL_MS__);
}}
metricFrames.textContent = Number(DATA.tpf).toFixed(2);
metricNfe.textContent = String(DATA.nfe);
metricGen.textContent = String(DATA.generated_token_ids.length);
metricThreshold.textContent = Number(DATA.threshold).toFixed(2);
promptBox.textContent = DATA.prompt;
slider.max = String(Math.max(DATA.frames.length - 1, 0));
renderFrame(0);
slider.addEventListener("input", (event) => {{
stopPlayback();
renderFrame(Number(event.target.value));
}});
playBtn.addEventListener("click", togglePlayback);
prevBtn.addEventListener("click", () => {{
stopPlayback();
renderFrame(Math.max(0, currentFrame - 1));
}});
nextBtn.addEventListener("click", () => {{
stopPlayback();
renderFrame(Math.min(DATA.frames.length - 1, currentFrame + 1));
}});
window.addEventListener("resize", () => {{
if (!answerBox.hidden) {{
fitFinalMarkdown();
}}
}});
</script>
</body>
</html>
"""
html = html.replace("{{", "{").replace("}}", "}")
html = html.replace("__PAYLOAD_JSON__", payload_json)
html = html.replace("__STAGE_SIDEBAR_WIDTH__", str(STAGE_SIDEBAR_WIDTH))
html = html.replace("__STAGE_WIDTH__", str(STAGE_WIDTH))
html = html.replace("__STAGE_MIN_HEIGHT__", str(STAGE_MIN_HEIGHT))
html = html.replace("__STAGE_FINAL_HEIGHT_VH__", str(STAGE_FINAL_HEIGHT_VH))
html = html.replace("__STAGE_FINAL_MAX_HEIGHT__", str(STAGE_FINAL_MAX_HEIGHT))
html = html.replace("__TOKEN_GRID_MIN_HEIGHT__", str(TOKEN_GRID_MIN_HEIGHT))
html = html.replace(
"__FINAL_MARKDOWN_BASE_FONT_SIZE__", str(FINAL_MARKDOWN_BASE_FONT_SIZE)
)
html = html.replace(
"__FINAL_MARKDOWN_MIN_FONT_SIZE__", str(FINAL_MARKDOWN_MIN_FONT_SIZE)
)
html = html.replace("__PLAYBACK_INTERVAL_MS__", str(PLAYBACK_INTERVAL_MS))
return html
def main():
args = parse_args()
prompt = load_prompt(args)
model = AutoModelForCausalLM.from_pretrained(
args.model_path,
trust_remote_code=True,
device_map=args.device,
)
model = model.to(torch.bfloat16)
model.eval()
tokenizer = AutoTokenizer.from_pretrained(args.model_path, trust_remote_code=True)
input_ids = tokenize_prompt(tokenizer, prompt)
demo_trace, nfe, generated_tokens = model.generate_uniform_demo(
inputs=input_ids,
gen_length=args.gen_length,
block_length=args.block_length,
steps=args.steps,
threshold=args.threshold,
)
generated_answer = tokenizer.decode(
generated_tokens[0],
skip_special_tokens=True,
clean_up_tokenization_spaces=False,
)
demo_trace = enrich_demo_trace_for_render(demo_trace)
token_labels = collect_token_labels(tokenizer, demo_trace)
payload = build_demo_payload(
prompt=prompt,
generated_answer=generated_answer,
demo_trace=demo_trace,
nfe=nfe,
token_labels=token_labels,
)
output_path = Path(args.output).expanduser().resolve()
output_path.write_text(render_html(payload), encoding="utf-8")
print(f"Saved demo to: {output_path}")
print(f"Frames: {len(demo_trace['frames'])}")
print(f"NFE: {nfe}")
print(f"Generated tokens: {generated_tokens.shape[1]}")
if __name__ == "__main__":
main()