SimSD
July 28, 2026 · View on GitHub
Speculative decoding for SDAR (Synergy of Diffusion and AutoRegression) models. A small draft model (SDAR-1.7B-Chat) runs block-diffusion denoising; a larger target model (SDAR-8B-Chat) verifies via multi-block causal attention in a single forward pass, with greedy-match acceptance + variable-length truncate commit. The default pipeline runs draft on one GPU and target on another with full stream overlap (incl. speculative target K/V extend).
Compared to the TP=2 vanilla baseline at ~9.6 tok/s, the optimized ours config
reaches 63–74 tok/s on a single 96 GB Blackwell pair, lossless w.r.t.
argmax-decoded target (token-by-token identical output).
1. Install
Tested on Python 3.10, CUDA 12.8 (NVIDIA RTX PRO 6000 Blackwell). The repo has no Python package of its own — it is run from the source tree.
# 1) Create env (conda or venv). Match the python / cuda versions if you can.
conda create -n simsd python=3.10 -y && conda activate simsd
# 2) Install PyTorch matching your CUDA toolkit (cu128 example below):
pip install torch==2.8.0 --index-url https://download.pytorch.org/whl/cu128
# 3) The rest of the deps:
pip install -r requirements.txt
flash-attn is required by the vendored SDAR modeling code (top-level
from flash_attn.ops.triton.layer_norm import rms_norm_fn). If the prebuilt
wheel does not match your torch/CUDA, build it from source:
pip install flash-attn==2.8.3 --no-build-isolation
Model weights
Download from HuggingFace (or point --draft_model / --target_model to your
local copies):
JetLM/SDAR-1.7B-Chat(draft)JetLM/SDAR-8B-Chat(target)
huggingface-cli download JetLM/SDAR-1.7B-Chat
huggingface-cli download JetLM/SDAR-8B-Chat
2. One-click reproduce main_table
python main.py
That single command runs all three methods (vanilla / vanilla_cg / ours)
× {latency, quality} over four datasets (gsm8k, mbpp, triviaqa,
mmlu) at N=200, writes per-job SUMMARY.json plus a top-level
UNIFIED.json, and prints a unified results table at the end. Throughput is
computed from torch.cuda.Event (not wall clock).
The settings come from configs/experiments/sdar.yaml, which main.py reads by
default — HuggingFace checkpoints (JetLM/SDAR-8B-Chat +
JetLM/SDAR-1.7B-Chat), GPUs 0,1, and the optimized ours config (fused
denoise + speculative target extend + truncate + greedy-match). Every value is
overridable on the CLI:
python main.py -e configs/experiments/sdar.yaml --num_samples 20 --datasets gsm8k
python main.py -e <file> --print_config # resolved settings + the exact commands
3. Other model families
Family-specific knowledge lives in speculative_decoding/adapters/; the adapter
is chosen by inspecting the loaded checkpoint, not by a flag. Adding a family
means adding one module there.
# LLaDA2.0-mini against itself — correctness harness for the pipeline
python main.py -e configs/experiments/llada2.yaml
# mini drafts, flash verifies (flash sharded over 4 GPUs)
python main.py -e configs/experiments/llada2_flash.yaml
# upstream generate() vs SimSD native vs self-draft, in one process
python scripts/llada2_selfdraft_check.py --dtype float32
Two LLaDA2 constraints are encoded in the configs rather than left to be rediscovered:
- Eager only.
LLaDA2MoeSparseMoeBlock.moe_inferdoestokens_per_expert.cpu().numpy()and loops over 256 experts in Python, so nothing can be captured into a cuda_graph. The adapter raises with that explanation instead of failing inside capture. - Sharded target. LLaDA2.0-flash is 191.6 GiB in bf16, so the TP=2 baselines
would need 95.8 GiB/rank against 95.6 GiB of usable HBM — it misses by a hair.
runtime.target_gpusspreads it over several cards instead withdevice_map="auto". That is naive model parallelism, not tensor parallelism and not pipeline parallelism either — layers are placed across devices but a single input runs through them strictly in order, so one GPU is busy at a time. It buys capacity, not speed. Compare againstnative_sharded, which places the target identically, rather than against a TP run; otherwise a placement difference gets attributed to speculation.
Notes
- Model weights, datasets and run outputs are not part of this repo
(see
.gitignore). Download them separately. - The IFEval scorer in
speculative_decoding/Experiment_Backend/self_draft_compare.pyrequires OpenCompass onsys.path. SetOPENCOMPASS_PATH=/path/to/opencompassbefore running if you want IFEval scoring; otherwise the loader is a no-op. vanilla/vanilla_cgrunners usetorchrun --nproc_per_node=2.oursis single-process dual-GPU (draft oncuda:0, target oncuda:1). The 4 GPU IDs you give via--gpusare split: first pair for vanilla/vanilla_cg, the same pair re-used (one process at a time) for ours.- vanilla_cg can hang on
dist.destroy_process_group()after writingSUMMARY.json(NCCL + cuda_graph teardown deadlock);main.pywatchdog detects this and force-kills after--cleanup_grace_s(default 90s).