From 6bebeb9afa9dcaa9f4ff445783a8809932803957 Mon Sep 17 00:00:00 2001 From: Chenhan Yu Date: Mon, 13 Jul 2026 13:46:05 -0700 Subject: [PATCH 1/6] docs: add announcements landing page Signed-off-by: Chenhan Yu --- docs/build_site.py | 340 +++++++++++++++ .../posts/2026-06-29-dspark-vs-domino.md | 222 ++++++++++ .../2026-07-13-github-pages-announcements.md | 34 ++ docs/site_src/static/css/blog.css | 401 ++++++++++++++++++ docs/site_src/static/js/blog.js | 57 +++ docs/site_src/tools/domino_fig.png | Bin 0 -> 178776 bytes .../tools/dspark_domino_al_qwen3_8b.png | Bin 0 -> 156764 bytes docs/site_src/tools/dspark_fig1.png | Bin 0 -> 111538 bytes docs/site_src/tools/dspark_fig7.png | Bin 0 -> 130179 bytes noxfile.py | 3 +- 10 files changed, 1056 insertions(+), 1 deletion(-) create mode 100644 docs/build_site.py create mode 100644 docs/site_src/posts/2026-06-29-dspark-vs-domino.md create mode 100644 docs/site_src/posts/2026-07-13-github-pages-announcements.md create mode 100644 docs/site_src/static/css/blog.css create mode 100644 docs/site_src/static/js/blog.js create mode 100644 docs/site_src/tools/domino_fig.png create mode 100644 docs/site_src/tools/dspark_domino_al_qwen3_8b.png create mode 100644 docs/site_src/tools/dspark_fig1.png create mode 100644 docs/site_src/tools/dspark_fig7.png diff --git a/docs/build_site.py b/docs/build_site.py new file mode 100644 index 00000000000..7cba6dcd92b --- /dev/null +++ b/docs/build_site.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Build the public Model Optimizer announcement site.""" + +from __future__ import annotations + +import argparse +import html +import json +import re +import shutil +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parent +SITE_SRC = ROOT / "site_src" +POSTS_DIR = SITE_SRC / "posts" +STATIC_DIR = SITE_SRC / "static" +TOOLS_DIR = SITE_SRC / "tools" +SOURCE_ASSETS = ROOT / "source" / "assets" + + +def parse_frontmatter(text: str) -> tuple[dict, str]: + if not text.startswith("---"): + return {}, text + end = text.find("\n---", 3) + if end < 0: + return {}, text + metadata = yaml.safe_load(text[3:end]) or {} + return metadata, text[end + 4 :].strip() + + +def slug_from_path(path: Path) -> str: + slug = path.stem + match = re.match(r"\d{4}-\d{2}-\d{2}-(.*)", slug) + return match.group(1) if match else slug + + +def inline_markup(text: str) -> str: + escaped = html.escape(text) + escaped = re.sub(r"`([^`]+)`", r"\1", escaped) + escaped = re.sub(r"\*\*([^*]+)\*\*", r"\1", escaped) + escaped = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', escaped) + return escaped + + +def page_path(path: str, prefix: str) -> str: + if not path or "://" in path or path.startswith("#") or path.startswith("mailto:"): + return path + if path.startswith("/"): + return prefix + path.lstrip("/") + return path + + +def render_markdown(markdown: str, prefix: str) -> str: + lines = markdown.splitlines() + out: list[str] = [] + paragraph: list[str] = [] + list_open = False + code_open = False + code_lines: list[str] = [] + + def flush_paragraph() -> None: + nonlocal paragraph + if paragraph: + out.append(f"

{inline_markup(' '.join(paragraph))}

") + paragraph = [] + + def close_list() -> None: + nonlocal list_open + if list_open: + out.append("") + list_open = False + + for raw in lines: + line = raw.rstrip() + stripped = line.strip() + if stripped.startswith("```"): + flush_paragraph() + close_list() + if code_open: + out.append(f"
{html.escape(chr(10).join(code_lines))}
") + code_lines = [] + code_open = False + else: + code_open = True + continue + if code_open: + code_lines.append(raw) + continue + if not stripped: + flush_paragraph() + close_list() + continue + if stripped.startswith("<") and stripped.endswith(">"): + flush_paragraph() + close_list() + out.append(re.sub(r'(src|href)="(/[^"]*)"', lambda m: f'{m.group(1)}="{page_path(m.group(2), prefix)}"', stripped)) + continue + if stripped.startswith("### "): + flush_paragraph() + close_list() + out.append(f"

{inline_markup(stripped[4:])}

") + continue + if stripped.startswith("## "): + flush_paragraph() + close_list() + out.append(f"

{inline_markup(stripped[3:])}

") + continue + image = re.match(r"!\[([^\]]*)\]\(([^)]+)\)", stripped) + if image: + flush_paragraph() + close_list() + alt, src = image.groups() + src = page_path(src, prefix) + out.append( + '
' + f'{html.escape(alt, quote=True)}' + f"
{inline_markup(alt)}
" + "
" + ) + continue + if stripped.startswith("- "): + flush_paragraph() + if not list_open: + out.append("") - list_open = False - - for raw in lines: - line = raw.rstrip() - stripped = line.strip() - if stripped.startswith("```"): - flush_paragraph() - close_list() - if code_open: - out.append(f"
{html.escape(chr(10).join(code_lines))}
") - code_lines = [] - code_open = False - else: - code_open = True - continue - if code_open: - code_lines.append(raw) - continue - if not stripped: - flush_paragraph() - close_list() - continue - if stripped.startswith("<") and stripped.endswith(">"): - flush_paragraph() - close_list() - out.append(re.sub(r'(src|href)="(/[^"]*)"', lambda m: f'{m.group(1)}="{page_path(m.group(2), prefix)}"', stripped)) - continue - if stripped.startswith("### "): - flush_paragraph() - close_list() - out.append(f"

{inline_markup(stripped[4:])}

") - continue - if stripped.startswith("## "): - flush_paragraph() - close_list() - out.append(f"

{inline_markup(stripped[3:])}

") - continue - image = re.match(r"!\[([^\]]*)\]\(([^)]+)\)", stripped) - if image: - flush_paragraph() - close_list() - alt, src = image.groups() - src = page_path(src, prefix) - out.append( - '
' - f'{html.escape(alt, quote=True)}' - f"
{inline_markup(alt)}
" - "
" - ) - continue - if stripped.startswith("- "): - flush_paragraph() - if not list_open: - out.append("
    ") - list_open = True - out.append(f"
  • {inline_markup(stripped[2:])}
  • ") - continue - paragraph.append(stripped) - - flush_paragraph() - close_list() - if code_open: - out.append(f"
    {html.escape(chr(10).join(code_lines))}
    ") - return "\n".join(out) - - -def load_posts() -> list[dict]: - posts = [] - for path in sorted(POSTS_DIR.glob("*.md")): - metadata, body = parse_frontmatter(path.read_text(encoding="utf-8")) - if not metadata.get("title"): - continue - slug = slug_from_path(path) - posts.append( - { - "title": str(metadata.get("title", "")), - "author": str(metadata.get("author", "")), - "date": str(metadata.get("date", ""))[:10], - "summary": str(metadata.get("summary", "")), - "tags": [str(tag) for tag in metadata.get("tags", [])], - "image": str(metadata.get("image", "")), - "slug": slug, - "url": f"announcements/{slug}/", - "body": body, - } - ) - posts.sort(key=lambda post: post["date"], reverse=True) - return posts - - -def topnav(prefix: str, active: str) -> str: - home = prefix or "./" - links = [ - ("Announcements", home, "announcements"), - ("API Docs", f"{prefix}api/", "api"), - ("GitHub", "https://github.com/NVIDIA/Model-Optimizer", "github"), - ] - rendered = [] - for label, href, key in links: - cls = ' class="active"' if active == key else "" - rendered.append(f'{label}') - return ( - '" - ) - - -def render_index(posts: list[dict]) -> str: - tags = sorted({tag for post in posts for tag in post["tags"]}) - cards = [] - for post in posts: - tag_html = "".join(f"#{html.escape(tag)}" for tag in post["tags"]) - cards.append( - '' - f'' - f'

    {html.escape(post["title"])}

    ' - f'

    {html.escape(post["summary"])}

    ' - f'
    {html.escape(post["author"])}
    {tag_html}
    ' - "
    " - ) - tag_buttons = "".join(f'' for tag in tags) - posts_json = json.dumps( - [ - { - "title": p["title"], - "summary": p["summary"], - "tags": p["tags"], - "url": p["url"], - "date": p["date"], - } - for p in posts - ] - ) - return f""" - - - - - Model Optimizer Announcements - - - - - {topnav("", "announcements")} -
    -
    -
    -

    NVIDIA Model Optimizer

    -

    Announcements

    -

    Release notes, technical updates, examples, and deployment stories from the Model Optimizer team.

    -
    -
    - -
    - -
    - - {tag_buttons} -
    -
    - -
    - {"".join(cards)} -
    - - - -
    -

    Add an announcement by PR

    -

    Create a Markdown file in docs/site_src/posts/ with YAML frontmatter. Images can live under docs/site_src/static/images/ and be referenced from the post body.

    -
    -
    - - - - -""" - - -def render_post(post: dict) -> str: - tags = "".join(f"#{html.escape(tag)}" for tag in post["tags"]) - prefix = "../../" - image_src = page_path(post["image"], prefix) - image = f'' if image_src else "" - content_html = render_markdown(post["body"], prefix) - return f""" - - - - - {html.escape(post["title"])} | Model Optimizer Announcements - - - - - {topnav(prefix, "announcements")} -
    - Back to announcements -
    - -

    {html.escape(post["title"])}

    -

    {html.escape(post["summary"])}

    - - {image} -
    -
    {content_html}
    -
    - - -""" - - -def build(output_dir: Path) -> None: - output_dir.mkdir(parents=True, exist_ok=True) - posts = load_posts() - (output_dir / "index.html").write_text(render_index(posts), encoding="utf-8") - for post in posts: - post_dir = output_dir / "announcements" / post["slug"] - post_dir.mkdir(parents=True, exist_ok=True) - (post_dir / "index.html").write_text(render_post(post), encoding="utf-8") - if STATIC_DIR.exists(): - shutil.copytree(STATIC_DIR, output_dir / "static", dirs_exist_ok=True) - if TOOLS_DIR.exists(): - shutil.copytree(TOOLS_DIR, output_dir / "tools", dirs_exist_ok=True) - if SOURCE_ASSETS.exists(): - image_dir = output_dir / "static" / "images" - image_dir.mkdir(parents=True, exist_ok=True) - for asset in SOURCE_ASSETS.iterdir(): - if asset.is_file(): - shutil.copy2(asset, image_dir / asset.name) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--output", type=Path, default=ROOT / "build" / "html") - args = parser.parse_args() - build(args.output) - - -if __name__ == "__main__": - main() diff --git a/docs/site_src/posts/2026-06-29-dspark-vs-domino.md b/docs/site_src/posts/2026-06-29-dspark-vs-domino.md deleted file mode 100644 index 217ff6e39c4..00000000000 --- a/docs/site_src/posts/2026-06-29-dspark-vs-domino.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: "DSpark vs Domino: Same DFlash Backbone, Different Correction Heads" -author: ModelOpt Team -date: 2026-06-29 -tags: [speculative-decoding, dflash, dspark, domino, architecture] -summary: "DSpark (DeepSpec) and Domino both build on block-parallel DFlash draft generation but diverge sharply in their token-level correction heads. DSpark uses a stateless VanillaMarkov head (fast, parallelizable); Domino uses a GRU (expressive, sequential). This post walks through the architecture diagram, the checkpoint weight anatomy, and when each design wins." -highlights: - - "Both systems share the DFlash block-parallel backbone — nearly identical parallel draft throughput" - - "DSpark defaults to VanillaMarkov: stateless W1/W2 embedding lookups, no hidden state to thread through" - - "Domino uses nn.GRU: token embeddings as input, draft hidden z_i concatenated at readout — true RNN" - - "Both correction heads are sequential at inference (x_{k-1} must be sampled before step k); DSpark's per-step cost is an O(1) embedding lookup vs Domino's full GRU cell" - - "DSpark adds a hardware-aware prefix scheduler (confidence_head) absent from Domino; vLLM does not plan to support this feature" - - "For dense models, DSpark simplifies to backbone + Markov head only — no mHC, no hc_head" ---- - -> **New to DFlash?** This post assumes familiarity with the DFlash block-parallel draft backbone. For a thorough introduction see [**DFlash: Block-Parallel Speculative Decoding for Nemotron**](/blog/dflash-presentation-for-nemotron-summit-diffusion-llm-sectio/) — a good starting point before diving into the comparison below. - -Both [DSpark (DeepSpec)](https://github.com/deepseek-ai/DeepSpec) and [Domino](https://github.com/jianuo-huang/Domino) are speculative decoding systems that pair a block-parallel draft backbone with a sequential correction head. From a distance they look identical. Up close, the correction heads are fundamentally different — and that difference drives the latency, complexity, and hardware tradeoffs. - -DSpark Figure 1: overall architecture and decoding cycle - -## Shared foundation: DFlash block-parallel backbone - -Both systems use **DFlash**: a draft backbone that runs a single causal attention forward pass over all γ draft positions in parallel, producing: - -- **h₁…h_γ** — per-position backbone hidden states -- **U₁…U_γ** — base draft logits - -This is the expensive step. Everything below is cheap correction on top of these parallel outputs. - -## Where they diverge: the correction head - -``` - ┌──────────────────────────────────────────────────────────────────────────────────────────┐ - │ DFlash Parallel Pass │ - │ x_0 ──► backbone ──► h_1, U_1 │ - │ x_0 ──► backbone ──► h_2, U_2 │ - │ … … │ - │ x_0 ──► backbone ──► h_γ, U_γ │ - └──────────────────────────────────────────────────────────────────────────────────────────┘ - │ - ┌───────────────────────────────┴───────────────────────────────┐ - │ │ - ┌────────────────────────▼───────────────────────┐ ┌────────────────────────▼───────────────────────┐ - │ DSpark — VanillaMarkov │ │ Domino — GRU │ - │ │ │ │ - │ For k = 1..γ: │ │ For k = 1..γ: │ - │ │ │ │ - │ e_{k-1} = W1[x_{k-1}] │ │ e_{k-1} = Emb[x_{k-1}] │ - │ │ │ │ - │ bias_k = W2 · e_{k-1} │ │ gru_h_k = GRU(e_{k-1}, │ - │ │ │ gru_h_{k-1}) │ - │ p_k = softmax(U_k + bias_k) │ │ │ - │ x_k ~ p_k │ │ p_k = softmax(U_k + W·[h_k; │ - │ │ │ gru_h_k]) │ - │ (depends on x_{k-1} only — │ │ x_k ~ p_k │ - │ no hidden state carried forward) │ │ │ - │ │ │ (full prefix history │ - │ │ │ accumulated in gru_h_{k-1}) │ - └─────────────────────────────────────────────────┘ └────────────────────────────────────────────────┘ -``` - -### DSpark — VanillaMarkov (first-order Markov head) - -The correction is a **stateless** first-order Markov transition. For each draft position k: - -``` -e_{k-1} = W1[x_{k-1}] # embedding lookup: previous token only -bias_k = W2 · e_{k-1} # transition bias (no hidden state) -p_k = softmax(U_k + bias_k) -x_k ~ p_k -``` - -`W1 ∈ ℝ^{V×r}` and `W2 ∈ ℝ^{V×r}` (r=512, V=129280 for DeepSeek-V4-Pro). The correction at position k depends **only on x_{k-1}** — no RNN hidden state threads across steps. This means the dominant computation (W1 embedding lookup + W2 projection) is a simple table lookup, not a recurrent rollout. - -DSpark also defines `GatedMarkov` and `RNNHead` as drop-in alternatives, but the released production checkpoint uses VanillaMarkov (confirmed in DSpark Section 4.3.2: *"we use the Markov head as the default"*). - -### Domino — GRU correction head - -Domino uses `nn.GRU` with **token embeddings as input** and the backbone draft hidden state z_i concatenated at readout: - -``` -gru_h_k = GRU(Emb[x_{k-1}], gru_h_{k-1}) # GRU: carries hidden state -p_k = softmax(U_k + W · [h_k; gru_h_k]) -x_k ~ p_k -``` - -The GRU accumulates a hidden state `gru_h_{k-1}` that carries information about the full prefix x_0…x_{k-1}. This is more expressive than VanillaMarkov — the correction can condition on the entire draft history, not just the immediately preceding token. - -Domino pipeline: DFlash backbone + GRU causal correction head - -The figure below shows training acceptance length (AL) on Qwen3-8B across three systems — DFlash baseline, Domino GRU, and our DSpark implementation. Both correction heads lift AL above the parallel backbone baseline, with the causal dependency injected by the correction head becoming visible as training progresses. - -Training acceptance length on Qwen3-8B: DFlash baseline vs Domino GRU vs DSpark - -## Correction head overhead: per-step cost - -Both correction heads are **strictly sequential at inference** — computing step k requires sampling x_{k-1} first, regardless of whether there is a recurrent hidden state. The difference is how expensive each step is: - -| System | Per-step compute | State carried | -|---|---|---| -| DSpark VanillaMarkov | Two embedding lookups: `W1[x_{k-1}]` + `W2 · e_{k-1}` | None — stateless | -| Domino GRU | Full GRU cell: input gate + reset gate + new gate over 4096-dim input, 1024-dim hidden | `gru_h_{k-1}` — 1024-dim hidden state | - -DSpark's Markov head adds only **0.2–1.3% latency** over DFlash at batch=128 (DSpark paper Figure 4) precisely because each step is an O(1) table lookup. Domino's GRU cell is heavier per step, though it remains small relative to the backbone cost. During **training** both can be parallelized via teacher forcing; the sequential constraint only bites at inference. - -## Additional DSpark machinery - -DSpark ships two components that Domino omits entirely: - -### Hardware-aware prefix scheduler (confidence_head) - -Unique to the **final MTP layer (mtp.2)**: - -``` -c_k = σ(w^T [hc_head(h_k); W1[x_{k-1}]]) # per-position acceptance probability -``` - -`hc_head` is a small 4-component calibration module that transforms `h_k` before the scalar confidence projection. The scheduler (Algorithm 1) uses `c_k` to dynamically pick how many draft tokens to submit for verification per request, maximizing throughput `Θ = τ · SPS(B)` across all concurrent requests. - -**If you disable the prefix scheduler, `confidence_head` and `hc_head` are never called.** Offline benchmarking always submits all γ tokens — the scheduler is a serving-time optimization. Notably, vLLM does not plan to support the hardware-aware prefix scheduler, so `confidence_head` and `hc_head` are effectively unused in vLLM-based deployments. - -### Manifold-constrained Hyper-Connections (mHC) - -For MoE models, the backbone transformer layers carry `hc_attn_*` and `hc_ffn_*` weights — the mHC parameters (Xie et al., 2026) applied to attention and FFN sublayers respectively. These are always active in the backbone forward pass. - -**For dense models, mHC is absent.** The `hc_attn_*` / `hc_ffn_*` weights are MoE-specific — they constrain connections across expert routing paths. A dense draft backbone (standard FFN layers) has no analog and would not carry these weights. - -## Checkpoint anatomy - -### Domino — `Qwen3-8B-Domino-b16` - -Reading the [released checkpoint](https://huggingface.co/Huang2020/Qwen3-8B-Domino-b16) reveals the exact forward pass (DFlash backbone layers excluded): - -| Weight | Shape | Derivation | -|---|---|---| -| `fc.weight` | [4096, 20480] | 20480 = 5 × 4096: fuses all 5 backbone layer outputs (concatenated) → z_k ∈ ℝ^{4096} | -| `hidden_norm.weight` | [4096] | LayerNorm on z_k before GRU | -| `prefix_gru.weight_ih_l0` | [3072, 4096] | GRU input gate: input_size=4096 (model dim), 3×hidden=3072 → hidden_size=1024 | -| `prefix_gru.weight_hh_l0` | [3072, 1024] | GRU hidden gate: confirms hidden_size=1024 | -| `embed_proj.0.weight` | [256, 5120] | 5120 = 4096 + 1024 = [z_k ; gru_h_k], projects to bottleneck 256 | -| `embed_proj.2.weight` | [151936, 256] | bottleneck → vocab (151936 = Qwen3-8B vocab), produces Δlogit_k | -| `norm.weight` | [4096] | LayerNorm on final output | - -The full correction pass for each draft position k: - -``` -z_k = fc(concat(h_k^{(1)}, ..., h_k^{(5)})) # fuse 5 backbone layers -z_k = hidden_norm(z_k) -gru_h_k = GRU(z_k, gru_h_{k-1}) # hidden_size=1024, carries prefix history -Δlogit_k = embed_proj([z_k; gru_h_k]) # [5120] → 256 → 151936 -p_k = softmax(U_k + Δlogit_k) -x_k ~ p_k -``` - -Two details are worth noting. First, the GRU input is `z_k` (the fused backbone hidden state), **not** a token embedding — which differs from the simplified description in the Domino README. Second, the `fc` fusion layer is what gives the GRU richer per-position context than a single layer's output. - -### DSpark — `DeepSeek-V4-Pro-DSpark` - -The DeepSeek-V4-Pro-DSpark checkpoint illustrates the full weight structure: - -| Weight | Shape | Role | Required? | -|---|---|---|---| -| `mtp.{i}.hc_attn_fn` | [24, 28672] | mHC on attention (MoE only) | Always (MoE) | -| `mtp.{i}.hc_ffn_fn` | [24, 28672] | mHC on FFN (MoE only) | Always (MoE) | -| `mtp.{i}.markov_head.markov_w1` | [129280, 512] | Markov embedding W1 | Always | -| `mtp.{i}.markov_head.markov_w2` | [129280, 512] | Markov projection W2 | Always | -| `mtp.2.hc_head_fn` | [4, 28672] | Calibration for confidence (last layer only) | Scheduler only | -| `mtp.2.confidence_head.proj` | [1, 7680] | Scalar confidence predictor | Scheduler only | - -For a **dense model without the prefix scheduler**, only `markov_w1` and `markov_w2` are active. - -## Production results (DSpark) - -DSpark Figure 7: throughput vs. TPS Pareto frontier - -## Takeaways - -1. **Same backbone, different correction heads.** DFlash draft generation is shared — if your baseline is the backbone, both systems are comparable. The correction head is the differentiator. - -2. **VanillaMarkov is cheaper per step than GRU — both are sequential.** Both heads must unroll left-to-right at inference (each step needs the previous sample). The difference is per-step cost: VanillaMarkov is two embedding lookups; Domino's GRU runs a full recurrent cell over a 4096-dim input with 1024-dim hidden state. At scale this per-step gap compounds across the draft block. - -3. **GRU is more expressive but the gain is marginal in practice.** DSpark's ablation (Section 4.3.2) explicitly validates that VanillaMarkov matches or beats RNNHead at the same draft quality. The extra expressiveness of a full RNN rarely translates to meaningfully higher acceptance rates on standard benchmarks. - -4. **DSpark's design is more general.** The pluggable Markov-head family (VanillaMarkov → GatedMarkov → RNNHead) means Domino's GRU approach is essentially one point in DSpark's design space — a specific `RNNHead` variant with embedding-only input. DSpark explored and explicitly rejected it as the default. - -5. **The prefix scheduler is a serving-time feature, not a draft-quality feature — and vLLM won't support it.** `confidence_head` and `hc_head` have zero effect on draft acceptance rate — they route how many tokens you *submit* for verification. vLLM does not plan to implement the hardware-aware prefix scheduler, so in practice these weights are dead weight for vLLM-based deployments. Disable the scheduler and you lose throughput optimization, not correctness. - -6. **Dense models get the simple version.** No mHC, no `hc_head` — just backbone + Markov head. The MoE-specific weights (`hc_attn_*`, `hc_ffn_*`, `hc_head_*`) are architectural additions for a specific deployment that happen to be visible in the DeepSeek-V4-Pro checkpoint. - -## Links - -- [DeepSpec / DSpark repo](https://github.com/deepseek-ai/DeepSpec) — model code + paper -- [DeepSeek-V4-Pro-DSpark checkpoint](https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-DSpark) — released MoE checkpoint with VanillaMarkov + mHC weights -- [Domino repo](https://github.com/jianuo-huang/Domino) — GRU-based correction head implementation -- [Domino checkpoint: Qwen3-8B-Domino-b16](https://huggingface.co/Huang2020/Qwen3-8B-Domino-b16) — SpecForge drafter format -- [ModelOpt PR #1710](https://github.com/NVIDIA/Model-Optimizer/pull/1710) — Domino training implementation (DFlash backbone + GRU correction head, dual loss curriculum) -- [Our DFlash reproduction on Qwen3.5-4B](/blog/dflash-qwen35-4b-reproduction) — training results and AL curves - -## Citations - -```bibtex -@misc{dspark2026, - title = {DSpark: Accelerating Large Language Model Inference with Speculative Decoding}, - author = {DeepSeek-AI}, - year = {2026}, - url = {https://github.com/deepseek-ai/DeepSpec/blob/main/DSpark_paper.pdf} -} - -@misc{domino2026, - title = {Domino: Accelerating Speculative Decoding with a Block-Parallel DFlash Backbone and GRU Correction Head}, - author = {Huang, Jianuo and others}, - year = {2026}, - url = {https://github.com/jianuo-huang/Domino} -} - -@misc{xie2026mhc, - title = {Manifold-Constrained Hyper-Connections}, - author = {Xie and others}, - year = {2026} -} -``` diff --git a/docs/site_src/posts/2026-07-13-github-pages-announcements.md b/docs/site_src/posts/2026-07-13-github-pages-announcements.md deleted file mode 100644 index 8e7a9f522d8..00000000000 --- a/docs/site_src/posts/2026-07-13-github-pages-announcements.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Model Optimizer announcements are moving to GitHub Pages" -author: Model Optimizer Team -date: 2026-07-13 -tags: [release, docs, github-pages] -summary: "The public Model Optimizer site is gaining a PR-updated announcements landing page while keeping API documentation one click away." -image: /static/images/model-optimizer-banner.png ---- - -The Model Optimizer GitHub Pages site is expanding from API documentation into a lightweight announcement hub. The goal is to make releases, technical notes, examples, and deployment writeups easier to discover without introducing a separate publishing system. - -## What changes - -- Announcements live as Markdown files reviewed through pull requests. -- The landing page defaults to announcements, with API documentation available from the top navigation. -- Posts support tags, search, filtering, and embedded images. - -![Model Optimizer banner](/static/images/model-optimizer-banner.png) - -## Authoring flow - -Create a Markdown file under `docs/site_src/posts/` with YAML frontmatter: - -``` ---- -title: "Your announcement" -author: Your Name -date: 2026-07-13 -tags: [quantization, release] -summary: "One sentence summary." ---- -``` - -The GitHub Pages workflow rebuilds the static site from the committed source, so every announcement follows the same review path as code and docs. diff --git a/docs/site_src/static/css/blog.css b/docs/site_src/static/css/blog.css deleted file mode 100644 index 0f0c22096c6..00000000000 --- a/docs/site_src/static/css/blog.css +++ /dev/null @@ -1,401 +0,0 @@ -:root { - --bg: #0f0f0f; - --bg-surface: #1a1a1a; - --bg-card: #222; - --bg-card-hover: #2a2a2a; - --text: #e8e8e8; - --text-muted: #a7a7a7; - --accent: #76b900; - --accent-blue: #00b4d8; - --border: #333; - --code-bg: #151515; - --max-width: 1080px; -} - -:root[data-theme="light"] { - --bg: #f7f8f5; - --bg-surface: #fff; - --bg-card: #fff; - --bg-card-hover: #f0f4ea; - --text: #161b12; - --text-muted: #536050; - --accent: #4d7f00; - --accent-blue: #007ea8; - --border: #d7decd; - --code-bg: #eef1ea; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - background: var(--bg); - color: var(--text); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - line-height: 1.65; - padding-top: 65px; -} - -.topnav { - align-items: center; - background: #1a1d28; - border-bottom: 1px solid #2d3148; - display: flex; - justify-content: space-between; - left: 0; - padding: 1rem 2rem; - position: fixed; - right: 0; - top: 0; - z-index: 20; -} - -:root[data-theme="light"] .topnav { - background: #ffffff; - border-bottom-color: #d7decd; - box-shadow: 0 1px 12px rgba(17, 24, 10, 0.06); -} - -.logo { - color: #76b900; - font-size: 1.05rem; - font-weight: 800; - text-decoration: none; -} - -:root[data-theme="light"] .logo { - color: #355f00; -} - -.nav-links { - align-items: center; - display: flex; - gap: 1.2rem; -} - -.nav-links a { - color: #94a3b8; - font-size: 0.9rem; - font-weight: 600; - text-decoration: none; -} - -:root[data-theme="light"] .nav-links a { - color: #596556; -} - -.nav-links a:hover, -.nav-links a.active { - color: #76b900; -} - -:root[data-theme="light"] .nav-links a:hover, -:root[data-theme="light"] .nav-links a.active { - color: #355f00; -} - -.theme-toggle { - align-items: center; - background: none; - border: 0; - color: #94a3b8; - cursor: pointer; - display: inline-flex; - font: inherit; - font-size: 0.82rem; - font-weight: 700; - gap: 0.45rem; - padding: 0; -} - -:root[data-theme="light"] .theme-toggle { - color: #596556; -} - -.theme-toggle:hover { - color: #76b900; -} - -.theme-toggle-track { - background: #2d3148; - border: 1px solid #3a405a; - border-radius: 999px; - display: inline-flex; - height: 22px; - padding: 2px; - width: 42px; -} - -.theme-toggle-thumb { - background: #76b900; - border-radius: 50%; - display: block; - height: 16px; - transform: translateX(0); - transition: transform 0.18s ease, background 0.18s ease; - width: 16px; -} - -:root[data-theme="light"] .theme-toggle-thumb { - background: #e2e8f0; - transform: translateX(18px); -} - -:root[data-theme="light"] .theme-toggle-track { - background: #76b900; - border-color: #76b900; -} - -main { - margin: 0 auto; - max-width: var(--max-width); - padding: 2rem 1.5rem 4rem; -} - -.blog-hero { - padding: 1.5rem 0 2.5rem; -} - -.eyebrow { - color: var(--accent); - font-size: 0.78rem; - font-weight: 800; - letter-spacing: 0.08em; - margin: 0 0 0.4rem; - text-transform: uppercase; -} - -.blog-hero h1 { - font-size: clamp(2.1rem, 7vw, 4.5rem); - line-height: 1; - margin: 0; -} - -.subtitle { - color: var(--text-muted); - font-size: 1.05rem; - max-width: 860px; -} - -.toolbar { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: 8px; - display: grid; - gap: 1rem; - margin-bottom: 1.25rem; - padding: 1rem; -} - -.search-box { - display: grid; - gap: 0.35rem; -} - -.search-box span { - color: var(--text-muted); - font-size: 0.78rem; - font-weight: 700; - text-transform: uppercase; -} - -.search-box input { - background: var(--bg); - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text); - font: inherit; - padding: 0.65rem 0.8rem; -} - -.search-box input:focus { - border-color: var(--accent); - outline: none; -} - -.tag-filter, -.card-tags { - display: flex; - flex-wrap: wrap; - gap: 0.45rem; -} - -.tag-filter button, -.card-tags span { - background: #1a2e00; - border: 1px solid #2a4a00; - border-radius: 4px; - color: #8fd400; - font-size: 0.75rem; - font-weight: 700; - padding: 0.2rem 0.55rem; -} - -:root[data-theme="light"] .tag-filter button, -:root[data-theme="light"] .card-tags span { - background: #edf6df; - border-color: #bed79a; - color: #3f6f00; -} - -:root[data-theme="light"] .tag-filter button.active, -:root[data-theme="light"] .tag-filter button:hover { - background: #4d7f00; - color: #fff; -} - -.tag-filter button { - cursor: pointer; -} - -.tag-filter button.active, -.tag-filter button:hover { - background: var(--accent); - color: #101010; -} - -.blog-grid { - display: grid; - gap: 1rem; -} - -.post-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text); - display: block; - padding: 1.35rem; - text-decoration: none; -} - -.post-card:hover { - background: var(--bg-card-hover); - border-color: var(--accent); -} - -.post-card time, -.post-header time { - color: var(--accent); - font-size: 0.78rem; - font-weight: 800; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.post-card h2 { - font-size: 1.35rem; - line-height: 1.25; - margin: 0.35rem 0; -} - -.post-card p, -.post-header p { - color: var(--text-muted); -} - -.card-meta, -.post-meta { - align-items: center; - color: var(--text-muted); - display: flex; - flex-wrap: wrap; - gap: 0.9rem; - margin-top: 0.8rem; -} - -.empty-state, -.authoring { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-muted); - margin-top: 1.5rem; - padding: 1rem; -} - -.authoring h2 { - color: var(--text); - margin-top: 0; -} - -code { - background: var(--code-bg); - border-radius: 4px; - padding: 0.1rem 0.35rem; -} - -.post-content { - max-width: 900px; -} - -.back-link { - color: var(--text-muted); - text-decoration: none; -} - -.back-link:hover { - color: var(--accent); -} - -.post-header { - border-bottom: 1px solid var(--border); - margin: 1.2rem 0 2rem; - padding-bottom: 1.5rem; -} - -.post-header h1 { - font-size: clamp(2rem, 6vw, 3.4rem); - line-height: 1.05; - margin: 0.35rem 0 0.6rem; -} - -.post-hero-image, -.post-image img { - border: 1px solid var(--border); - border-radius: 8px; - margin-top: 1rem; - max-width: 100%; -} - -article h2 { - margin-top: 2rem; -} - -article a { - color: var(--accent); -} - -article pre { - background: var(--code-bg); - border: 1px solid var(--border); - border-radius: 8px; - overflow-x: auto; - padding: 1rem; -} - -article pre code { - padding: 0; -} - -.post-image { - margin: 1.5rem 0; -} - -.post-image figcaption { - color: var(--text-muted); - font-size: 0.85rem; - margin-top: 0.35rem; -} - -@media (max-width: 760px) { - .topnav, - .nav-links { - align-items: flex-start; - flex-direction: column; - gap: 0.6rem; - } - -} diff --git a/docs/site_src/static/js/blog.js b/docs/site_src/static/js/blog.js deleted file mode 100644 index 3e776e66ae5..00000000000 --- a/docs/site_src/static/js/blog.js +++ /dev/null @@ -1,57 +0,0 @@ -(function () { - var themeButton = document.querySelector(".theme-toggle"); - function setTheme(theme) { - document.documentElement.dataset.theme = theme; - localStorage.setItem("modelopt-theme", theme); - if (themeButton) { - var next = theme === "dark" ? "Light" : "Dark"; - var label = themeButton.querySelector(".theme-toggle-label"); - if (label) label.textContent = next; - themeButton.setAttribute("aria-pressed", String(theme === "light")); - } - } - setTheme(localStorage.getItem("modelopt-theme") || document.documentElement.dataset.theme || "dark"); - if (themeButton) { - themeButton.addEventListener("click", function () { - setTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark"); - }); - } - - var search = document.getElementById("post-search"); - var grid = document.getElementById("post-grid"); - var empty = document.getElementById("empty-state"); - var filter = document.getElementById("tag-filter"); - if (!search || !grid || !filter) return; - - var activeTag = ""; - - function applyFilters() { - var query = search.value.trim().toLowerCase(); - var visible = 0; - grid.querySelectorAll(".post-card").forEach(function (card) { - var haystack = [ - card.getAttribute("data-title") || "", - card.getAttribute("data-summary") || "", - card.getAttribute("data-tags") || "", - ].join(" "); - var tags = card.getAttribute("data-tags") || ""; - var matchesQuery = !query || haystack.indexOf(query) !== -1; - var matchesTag = !activeTag || tags.split(/\s+/).indexOf(activeTag) !== -1; - var show = matchesQuery && matchesTag; - card.hidden = !show; - if (show) visible += 1; - }); - empty.hidden = visible !== 0; - } - - search.addEventListener("input", applyFilters); - filter.addEventListener("click", function (event) { - var button = event.target.closest("button[data-tag]"); - if (!button) return; - filter.querySelectorAll("button").forEach(function (item) { - item.classList.toggle("active", item === button); - }); - activeTag = button.getAttribute("data-tag") || ""; - applyFilters(); - }); -})(); diff --git a/docs/source/_static/announcements.css b/docs/source/_static/announcements.css new file mode 100644 index 00000000000..dcaf46d4ecd --- /dev/null +++ b/docs/source/_static/announcements.css @@ -0,0 +1,95 @@ +/* Scoped announcement styles for the Sphinx RTD theme. */ +.announcement-toolbar { + border: 1px solid #d6d8dc; + border-radius: 4px; + margin: 1.5rem 0; + padding: 1rem; + background: #f8f9fb; +} + +.announcement-search-label { + display: block; + font-weight: 700; + margin-bottom: 0.35rem; +} + +.announcement-search { + box-sizing: border-box; + width: 100%; + padding: 0.55rem 0.65rem; + border: 1px solid #b8bdc6; + border-radius: 4px; + font-size: 1rem; +} + +.announcement-tags { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-top: 0.75rem; +} + +.announcement-tag { + border: 1px solid #9aa1ad; + border-radius: 999px; + padding: 0.28rem 0.65rem; + background: #fff; + color: #2f343d; + cursor: pointer; + font-size: 0.86rem; +} + +.announcement-tag.is-active, +.announcement-tag:hover { + border-color: #76b900; + background: #76b900; + color: #111; +} + +.announcement-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; + margin: 1rem 0 2rem; +} + +.announcement-card { + border: 1px solid #d6d8dc; + border-radius: 4px; + padding: 1rem; + background: #fff; +} + +.announcement-card h2 { + margin-top: 0.25rem; + font-size: 1.2rem; + line-height: 1.35; +} + +.announcement-card p { + margin-bottom: 0.75rem; +} + +.announcement-card-meta { + color: #6b7280; + font-size: 0.85rem; +} + +.announcement-card-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.announcement-card-tags span { + border: 1px solid #d6d8dc; + border-radius: 999px; + color: #4b5563; + font-size: 0.78rem; + padding: 0.15rem 0.45rem; +} + +.announcement-empty { + border-left: 4px solid #76b900; + padding-left: 0.75rem; +} diff --git a/docs/source/_static/announcements.js b/docs/source/_static/announcements.js new file mode 100644 index 00000000000..e4fa3d41957 --- /dev/null +++ b/docs/source/_static/announcements.js @@ -0,0 +1,40 @@ +document.addEventListener('DOMContentLoaded', () => { + const search = document.querySelector('#announcement-search'); + const cards = Array.from(document.querySelectorAll('.announcement-card')); + const tags = Array.from(document.querySelectorAll('.announcement-tag')); + const empty = document.querySelector('#announcement-empty'); + let activeTag = 'all'; + + if (!search || cards.length === 0) { + return; + } + + const update = () => { + const query = search.value.trim().toLowerCase(); + let visible = 0; + + cards.forEach((card) => { + const haystack = [card.dataset.title, card.dataset.summary, card.dataset.tags].join(' ').toLowerCase(); + const tagMatch = activeTag === 'all' || (card.dataset.tags || '').split(' ').includes(activeTag); + const searchMatch = !query || haystack.includes(query); + const show = tagMatch && searchMatch; + card.hidden = !show; + if (show) visible += 1; + }); + + if (empty) { + empty.hidden = visible !== 0; + } + }; + + tags.forEach((button) => { + button.addEventListener('click', () => { + activeTag = button.dataset.tag || 'all'; + tags.forEach((tag) => tag.classList.toggle('is-active', tag === button)); + update(); + }); + }); + + search.addEventListener('input', update); + update(); +}); diff --git a/docs/site_src/tools/domino_fig.png b/docs/source/announcements/assets/domino_fig.png similarity index 100% rename from docs/site_src/tools/domino_fig.png rename to docs/source/announcements/assets/domino_fig.png diff --git a/docs/site_src/tools/dspark_domino_al_qwen3_8b.png b/docs/source/announcements/assets/dspark_domino_al_qwen3_8b.png similarity index 100% rename from docs/site_src/tools/dspark_domino_al_qwen3_8b.png rename to docs/source/announcements/assets/dspark_domino_al_qwen3_8b.png diff --git a/docs/site_src/tools/dspark_fig1.png b/docs/source/announcements/assets/dspark_fig1.png similarity index 100% rename from docs/site_src/tools/dspark_fig1.png rename to docs/source/announcements/assets/dspark_fig1.png diff --git a/docs/site_src/tools/dspark_fig7.png b/docs/source/announcements/assets/dspark_fig7.png similarity index 100% rename from docs/site_src/tools/dspark_fig7.png rename to docs/source/announcements/assets/dspark_fig7.png diff --git a/docs/source/announcements/dspark-vs-domino.rst b/docs/source/announcements/dspark-vs-domino.rst new file mode 100644 index 00000000000..bb8eb7b3834 --- /dev/null +++ b/docs/source/announcements/dspark-vs-domino.rst @@ -0,0 +1,105 @@ +DSpark vs Domino: Same DFlash Backbone, Different Correction Heads +################################################################## + +:Author: ModelOpt Team +:Date: June 29, 2026 +:Tags: speculative-decoding, dflash, dspark, domino, architecture + +DSpark (DeepSpec) and Domino both build on block-parallel DFlash draft generation but diverge sharply in their token-level correction heads. DSpark uses a stateless VanillaMarkov head that is fast and parallelizable during training; Domino uses a GRU that is more expressive but sequential at inference. + +Highlights +********** + +* Both systems share the DFlash block-parallel backbone, so their parallel draft throughput starts from a similar foundation. +* DSpark defaults to VanillaMarkov: stateless ``W1`` and ``W2`` embedding lookups with no hidden state to thread through. +* Domino uses ``nn.GRU`` and carries recurrent state across draft positions. +* Both correction heads are sequential at inference because ``x_{k-1}`` must be sampled before step ``k``. +* DSpark adds a hardware-aware prefix scheduler through ``confidence_head``; Domino does not include this mechanism. + +Shared Foundation: DFlash Block-Parallel Backbone +************************************************* + +Both systems use DFlash: a draft backbone that runs a single causal attention forward pass over all draft positions in parallel, producing per-position hidden states and base draft logits. This is the expensive step; the correction head adds token-level adjustment on top of those outputs. + +.. image:: assets/dspark_fig1.png + :alt: DSpark overall architecture and decoding cycle + :width: 100% + +Where They Diverge: The Correction Head +*************************************** + +DSpark uses a first-order Markov transition. For each draft position ``k``: + +.. code-block:: text + + e_{k-1} = W1[x_{k-1}] + bias_k = W2 * e_{k-1} + p_k = softmax(U_k + bias_k) + x_k ~ p_k + +The correction at position ``k`` depends only on ``x_{k-1}``; no RNN hidden state threads across steps. The dominant work is a table lookup and projection rather than a recurrent rollout. + +Domino uses a GRU correction head. A recurrent hidden state accumulates information about the draft prefix and is concatenated at readout: + +.. code-block:: text + + gru_h_k = GRU(input_k, gru_h_{k-1}) + p_k = softmax(U_k + W * [h_k; gru_h_k]) + x_k ~ p_k + +.. image:: assets/domino_fig.png + :alt: Domino pipeline with a DFlash backbone and GRU causal correction head + :width: 100% + +The figure below compares training acceptance length on Qwen3-8B across the DFlash baseline, Domino GRU, and a DSpark implementation. + +.. image:: assets/dspark_domino_al_qwen3_8b.png + :alt: Training acceptance length on Qwen3-8B: DFlash baseline vs Domino GRU vs DSpark + :width: 100% + +Correction Head Overhead +************************ + +.. list-table:: + :header-rows: 1 + + * - System + - Per-step compute + - State carried + * - DSpark VanillaMarkov + - ``W1[x_{k-1}]`` plus transition projection + - None + * - Domino GRU + - Full GRU cell over a high-dimensional input + - Recurrent hidden state + +Both heads must unroll left-to-right at inference. The practical difference is per-step cost: VanillaMarkov is much lighter, while the GRU can condition on a richer prefix history. + +Additional DSpark Machinery +*************************** + +DSpark includes a hardware-aware prefix scheduler through a confidence head. The scheduler estimates acceptance probability and selects how many draft tokens to submit for verification. It is a serving-time throughput optimization, not a draft-quality feature. + +For MoE models, DSpark checkpoints also include manifold-constrained Hyper-Connections. Dense models use the simpler backbone plus Markov-head path. + +.. image:: assets/dspark_fig7.png + :alt: DSpark throughput and TPS Pareto frontier + :width: 100% + +Takeaways +********* + +#. DFlash draft generation is shared; the correction head is the main differentiator. +#. VanillaMarkov is cheaper per step than GRU, although both are sequential at inference. +#. GRU is more expressive, but the extra recurrence may not translate into a large acceptance-rate gain. +#. DSpark's design is broader: VanillaMarkov, GatedMarkov, and RNN-style heads all fit the same family. +#. The prefix scheduler affects serving throughput decisions, not correctness. + +Links +***** + +* `DeepSpec / DSpark repo `_ +* `DeepSeek-V4-Pro-DSpark checkpoint `_ +* `Domino repo `_ +* `Domino checkpoint: Qwen3-8B-Domino-b16 `_ +* `ModelOpt PR #1710 `_ diff --git a/docs/source/announcements/github-pages-announcements.rst b/docs/source/announcements/github-pages-announcements.rst new file mode 100644 index 00000000000..03f9394ff2b --- /dev/null +++ b/docs/source/announcements/github-pages-announcements.rst @@ -0,0 +1,21 @@ +Model Optimizer Announcements Are Moving to GitHub Pages +######################################################### + +:Author: Model Optimizer Team +:Date: July 13, 2026 +:Tags: release, docs, github-pages + +The Model Optimizer GitHub Pages site is expanding from API documentation into a lightweight announcement hub. The goal is to make releases, technical notes, examples, and deployment writeups easier to discover without introducing a separate publishing system. + +What Changes +************ + +* Announcements live in the documentation source and are reviewed through pull requests. +* The landing page defaults to announcements. +* Existing API documentation remains available from the Sphinx left navigation. +* Announcement pages support tags, search, filtering, and embedded images. + +Authoring Flow +************** + +Add a Sphinx page under ``docs/source/announcements/`` and link it from the announcements toctree in ``docs/source/index.rst``. The GitHub Pages workflow rebuilds the static site from committed source, so every announcement follows the same review path as code and docs. diff --git a/docs/source/conf.py b/docs/source/conf.py index e4b68ce7643..9f24b299fcb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -116,7 +116,8 @@ html_static_path = ["_static"] html_title = f"Model Optimizer {version}" -html_css_files = ["custom.css"] +html_css_files = ["custom.css", "announcements.css"] +html_js_files = ["announcements.js"] html_permalinks_icon = "#" # default icon not rendering properly # TODO: left here as reference for future diff --git a/docs/source/index.rst b/docs/source/index.rst index 80b27a851df..8f8e5097e91 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,45 @@ -Welcome to Model Optimizer (ModelOpt) documentation! -#################################################### +Announcements +############# + +Release notes, technical updates, examples, and deployment stories from the Model Optimizer team. + +.. raw:: html + +
    + + +
    + + + + +
    +
    + +
    + + +
    + + + +.. toctree:: + :maxdepth: 1 + :caption: Announcements + + self + announcements/dspark-vs-domino + announcements/github-pages-announcements .. toctree:: :glob: @@ -38,7 +78,6 @@ Welcome to Model Optimizer (ModelOpt) documentation! examples/[0-9]* - .. toctree:: :glob: :maxdepth: 1 diff --git a/noxfile.py b/noxfile.py index f3be20fdaec..fbaba627d21 100644 --- a/noxfile.py +++ b/noxfile.py @@ -184,12 +184,11 @@ def docs(session): "-d", "/tmp/doctrees", "source", - "build/html/api", + "build/html", "--fail-on-warning", "--show-traceback", "--keep-going", ) - session.run("python", "build_site.py", "--output", "build/html") @nox.session From b7387d93a1dacc1d57d1dee0018e5311f8b9dfef Mon Sep 17 00:00:00 2001 From: Chenhan Yu Date: Tue, 14 Jul 2026 07:45:09 -0700 Subject: [PATCH 3/6] docs: simplify announcement navigation Signed-off-by: Chenhan Yu --- docs/source/announcements/dspark-vs-domino.rst | 2 ++ docs/source/announcements/github-pages-announcements.rst | 2 ++ docs/source/index.rst | 9 +++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/source/announcements/dspark-vs-domino.rst b/docs/source/announcements/dspark-vs-domino.rst index bb8eb7b3834..1c11570c3ea 100644 --- a/docs/source/announcements/dspark-vs-domino.rst +++ b/docs/source/announcements/dspark-vs-domino.rst @@ -1,3 +1,5 @@ +:orphan: + DSpark vs Domino: Same DFlash Backbone, Different Correction Heads ################################################################## diff --git a/docs/source/announcements/github-pages-announcements.rst b/docs/source/announcements/github-pages-announcements.rst index 03f9394ff2b..191c74dd627 100644 --- a/docs/source/announcements/github-pages-announcements.rst +++ b/docs/source/announcements/github-pages-announcements.rst @@ -1,3 +1,5 @@ +:orphan: + Model Optimizer Announcements Are Moving to GitHub Pages ######################################################### diff --git a/docs/source/index.rst b/docs/source/index.rst index 8f8e5097e91..e001fcc3b1f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -34,14 +34,14 @@ Release notes, technical updates, examples, and deployment stories from the Mode .. toctree:: + :hidden: :maxdepth: 1 :caption: Announcements self - announcements/dspark-vs-domino - announcements/github-pages-announcements .. toctree:: + :hidden: :glob: :maxdepth: 1 :caption: Getting Started @@ -58,6 +58,7 @@ Release notes, technical updates, examples, and deployment stories from the Mode Quick Start: Sparsity .. toctree:: + :hidden: :glob: :maxdepth: 1 :caption: Guides @@ -65,6 +66,7 @@ Release notes, technical updates, examples, and deployment stories from the Mode guides/[0-9]* .. toctree:: + :hidden: :glob: :maxdepth: 1 :caption: Deployment @@ -72,6 +74,7 @@ Release notes, technical updates, examples, and deployment stories from the Mode deployment/[0-9]* .. toctree:: + :hidden: :glob: :maxdepth: 1 :caption: Examples @@ -79,6 +82,7 @@ Release notes, technical updates, examples, and deployment stories from the Mode examples/[0-9]* .. toctree:: + :hidden: :glob: :maxdepth: 1 :caption: Reference @@ -86,6 +90,7 @@ Release notes, technical updates, examples, and deployment stories from the Mode reference/[0-9]* .. toctree:: + :hidden: :glob: :maxdepth: 1 :caption: Support From 0fcf221a1e12d930bd01dcc8e2df904685569d30 Mon Sep 17 00:00:00 2001 From: Chenhan Yu Date: Tue, 14 Jul 2026 07:56:42 -0700 Subject: [PATCH 4/6] docs: refine announcement listing Signed-off-by: Chenhan Yu --- docs/source/_static/announcements.css | 36 +++++++++++- docs/source/_static/announcements.js | 79 ++++++++++++++++++++++++--- docs/source/index.rst | 5 ++ 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/docs/source/_static/announcements.css b/docs/source/_static/announcements.css index dcaf46d4ecd..82ed911b1ae 100644 --- a/docs/source/_static/announcements.css +++ b/docs/source/_static/announcements.css @@ -48,9 +48,9 @@ .announcement-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + grid-template-columns: minmax(0, 1fr); gap: 1rem; - margin: 1rem 0 2rem; + margin: 1rem 0 1.25rem; } .announcement-card { @@ -93,3 +93,35 @@ border-left: 4px solid #76b900; padding-left: 0.75rem; } + + +.announcement-pager { + align-items: center; + display: flex; + gap: 0.75rem; + justify-content: flex-end; + margin: 0 0 2rem; +} + +.announcement-page-button { + border: 1px solid #9aa1ad; + border-radius: 4px; + background: #fff; + color: #2f343d; + cursor: pointer; + padding: 0.35rem 0.7rem; +} + +.announcement-page-button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.announcement-page-status { + color: #4b5563; + font-size: 0.9rem; +} + +.toctree-wrapper.compound:empty { + display: none; +} diff --git a/docs/source/_static/announcements.js b/docs/source/_static/announcements.js index e4fa3d41957..b7dffcb459c 100644 --- a/docs/source/_static/announcements.js +++ b/docs/source/_static/announcements.js @@ -1,40 +1,101 @@ document.addEventListener('DOMContentLoaded', () => { + const trimAnnouncementPostSidebar = () => { + if (!window.location.pathname.includes('/announcements/')) { + return; + } + + const menu = document.querySelector('.wy-menu-vertical'); + if (!menu) { + return; + } + + menu.innerHTML = ` +

    Announcements

    + + `; + }; + + trimAnnouncementPostSidebar(); + const search = document.querySelector('#announcement-search'); const cards = Array.from(document.querySelectorAll('.announcement-card')); const tags = Array.from(document.querySelectorAll('.announcement-tag')); const empty = document.querySelector('#announcement-empty'); + const pager = document.querySelector('#announcement-pager'); + const prev = document.querySelector('#announcement-prev'); + const next = document.querySelector('#announcement-next'); + const status = document.querySelector('#announcement-page-status'); + const pageSize = 5; let activeTag = 'all'; + let currentPage = 1; if (!search || cards.length === 0) { return; } - const update = () => { + const matchingCards = () => { const query = search.value.trim().toLowerCase(); - let visible = 0; - - cards.forEach((card) => { + return cards.filter((card) => { const haystack = [card.dataset.title, card.dataset.summary, card.dataset.tags].join(' ').toLowerCase(); const tagMatch = activeTag === 'all' || (card.dataset.tags || '').split(' ').includes(activeTag); const searchMatch = !query || haystack.includes(query); - const show = tagMatch && searchMatch; - card.hidden = !show; - if (show) visible += 1; + return tagMatch && searchMatch; + }); + }; + + const update = () => { + const matches = matchingCards(); + const pageCount = Math.max(1, Math.ceil(matches.length / pageSize)); + currentPage = Math.min(currentPage, pageCount); + const start = (currentPage - 1) * pageSize; + const pageCards = new Set(matches.slice(start, start + pageSize)); + + cards.forEach((card) => { + card.hidden = !pageCards.has(card); }); if (empty) { - empty.hidden = visible !== 0; + empty.hidden = matches.length !== 0; + } + + if (pager && prev && next && status) { + pager.hidden = matches.length <= pageSize; + prev.disabled = currentPage <= 1; + next.disabled = currentPage >= pageCount; + status.textContent = `Page ${currentPage} of ${pageCount}`; } }; tags.forEach((button) => { button.addEventListener('click', () => { activeTag = button.dataset.tag || 'all'; + currentPage = 1; tags.forEach((tag) => tag.classList.toggle('is-active', tag === button)); update(); }); }); - search.addEventListener('input', update); + search.addEventListener('input', () => { + currentPage = 1; + update(); + }); + + if (prev) { + prev.addEventListener('click', () => { + currentPage -= 1; + update(); + }); + } + + if (next) { + next.addEventListener('click', () => { + currentPage += 1; + update(); + }); + } + update(); }); diff --git a/docs/source/index.rst b/docs/source/index.rst index e001fcc3b1f..56ae0eb7af1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,6 +32,11 @@ Release notes, technical updates, examples, and deployment stories from the Mode + .. toctree:: :hidden: From d25441e0574ab09d734f10140ebad5f910a72d40 Mon Sep 17 00:00:00 2001 From: Chenhan Yu Date: Tue, 14 Jul 2026 08:19:39 -0700 Subject: [PATCH 5/6] docs: sort announcements by date Signed-off-by: Chenhan Yu --- docs/source/_static/announcements.css | 10 ++++++---- docs/source/_static/announcements.js | 6 +++++- docs/source/index.rst | 14 +++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/source/_static/announcements.css b/docs/source/_static/announcements.css index 82ed911b1ae..d32702fa41a 100644 --- a/docs/source/_static/announcements.css +++ b/docs/source/_static/announcements.css @@ -54,10 +54,12 @@ } .announcement-card { - border: 1px solid #d6d8dc; - border-radius: 4px; - padding: 1rem; - background: #fff; + border-bottom: 1px solid #d6d8dc; + padding: 0 0 1rem; +} + +.announcement-card:last-child { + border-bottom: 0; } .announcement-card h2 { diff --git a/docs/source/_static/announcements.js b/docs/source/_static/announcements.js index b7dffcb459c..be03c9b3bd5 100644 --- a/docs/source/_static/announcements.js +++ b/docs/source/_static/announcements.js @@ -21,7 +21,9 @@ document.addEventListener('DOMContentLoaded', () => { trimAnnouncementPostSidebar(); const search = document.querySelector('#announcement-search'); - const cards = Array.from(document.querySelectorAll('.announcement-card')); + const cards = Array.from(document.querySelectorAll('.announcement-card')).sort((left, right) => { + return (right.dataset.date || '').localeCompare(left.dataset.date || ''); + }); const tags = Array.from(document.querySelectorAll('.announcement-tag')); const empty = document.querySelector('#announcement-empty'); const pager = document.querySelector('#announcement-pager'); @@ -29,6 +31,8 @@ document.addEventListener('DOMContentLoaded', () => { const next = document.querySelector('#announcement-next'); const status = document.querySelector('#announcement-page-status'); const pageSize = 5; + + cards.forEach((card) => card.parentNode.appendChild(card)); let activeTag = 'all'; let currentPage = 1; diff --git a/docs/source/index.rst b/docs/source/index.rst index 56ae0eb7af1..58577af35ac 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,18 +17,18 @@ Release notes, technical updates, examples, and deployment stories from the Mode
    - -
    + +
    From 890ae45b5f913d29a5b10c17c780c1af85c5bfb7 Mon Sep 17 00:00:00 2001 From: Chenhan Yu Date: Tue, 14 Jul 2026 11:43:25 -0700 Subject: [PATCH 6/6] docs: center announcement content Signed-off-by: Chenhan Yu --- docs/source/_static/announcements.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/_static/announcements.css b/docs/source/_static/announcements.css index d32702fa41a..13c1df7e0f6 100644 --- a/docs/source/_static/announcements.css +++ b/docs/source/_static/announcements.css @@ -1,4 +1,17 @@ /* Scoped announcement styles for the Sphinx RTD theme. */ + +#announcements > h1, +#announcements > p, +#announcements > .announcement-toolbar, +#announcements > .announcement-grid, +#announcements > .announcement-empty, +#announcements > .announcement-pager, +#announcements > .toctree-wrapper { + max-width: 860px; + margin-left: auto; + margin-right: auto; +} + .announcement-toolbar { border: 1px solid #d6d8dc; border-radius: 4px;