I compared three types of speculative decoding (DSpark / DFlash / MTP) of NVIDIA Nemotron 3.5 Lightning on DGX Spark

I compared three types of speculative decoding (DSpark / DFlash / MTP) of NVIDIA Nemotron 3.5 Lightning on DGX Spark

I actually compared the three types of speculative decoding methods bundled with NVIDIA's new open model Nemotron 3.5 Lightning on DGX Spark.
2026.08.12

This page has been translated by machine translation. View original

I am Okuri, who deeply loves whiskey, cigars, and pipes. I recently joined the Manufacturing Business Technology Department.

On August 11, 2026, NVIDIA announced a new open model, Nemotron 3.5 Lightning. This model comes bundled with three implementations of speculative decoding, and the official documentation even provides hardware-specific recommendations, stating "DSpark is recommended for DGX Spark." I ran a hands-on comparison of all three methods on my DGX Spark.

What is NVIDIA Nemotron 3.5 Lightning

Nemotron 3.5 Lightning is an open model designed to serve as the "execution layer" for always-on AI agents.

Long-running agents spend the majority of their execution time on high-frequency, routine operations such as tool calls, result validation, and delegation to sub-agents. Using a frontier reasoning model for each of these steps drives up both cost and latency. The intended division of labor is therefore to have frontier models like Nemotron 3 Ultra handle planning and complex decisions, while a smaller model like Lightning handles the high-frequency execution work.

The model specifications are as follows.

Item Details
Parameters 30B (active 3B)
Architecture Hybrid MoE combining Mamba-2, MoE, and Attention
Context length Up to 1M tokens
Pre-training 20T+ tokens with NVFP4 recipe
Quantization NVFP4 / BF16
Single-GPU deployment 1x DGX Spark (GB10) or 1x H100
Supported hardware Blackwell (GB10, GB200, GeForce RTX 5090), Hopper (H100, H200), Ampere with W4A16
Supported languages English (and coding languages), Spanish, French, German, Italian, Japanese
License OpenMDW-1.1
Release date August 11, 2026

One thing worth noting is that the "Single-GPU Deployment" field of the model card reads "1× DGX Spark (GB10) or 1× H100" — the datacenter-grade H100 and the desktop mini PC DGX Spark are listed side by side. The Quick Start section also puts DGX Spark first, with H100 and GB200 following afterward. This is clearly a model where DGX Spark is treated as the primary target.

My colleague Morishige has written a blog post covering the details of Nemotron 3.5 Lightning 30B-A3B-NVFP4, so please refer to that for more information.

https://dev.classmethod.jp/articles/dgx-spark-nemotron-3-5-lightning-first-touch/

Why speculative decoding is effective on DGX Spark

Before diving into the actual measurements, let me explain why speculative decoding is effective here. This is the key point of this comparison.

The DGX Spark's GB10 Grace Blackwell Superchip uses a unified memory architecture where 128GB of LPDDR5x is shared between the CPU and GPU. While 128GB is a large amount of memory, being LPDDR5x means memory bandwidth is capped at 273 GB/s. Compared to datacenter-grade GPUs equipped with HBM, this becomes a bottleneck.

LLM decoding is inherently memory-bandwidth-bound because the model weights must be read from memory for every token generated. If you were to run a dense 30B model quantized to NVFP4, the weights would be roughly 15GB. Reading 15GB per token at 273 GB/s gives a theoretical maximum of around 18 tokens/second — not particularly fast for a lightweight model.

Nemotron 3.5 Lightning is designed to work around this ceiling through several mechanisms.

Lightning design How it addresses memory bandwidth constraints
MoE 30B / active 3B The router sends each token to only a subset of experts, so the weights read per token are roughly one-tenth of the total
Mamba-2 hybrid Mamba layers only maintain a fixed-size state with no growing KV cache. Since only some layers use Attention, memory usage doesn't explode linearly with long contexts
Speculative decoding Multiple tokens are committed per weight read. This reduces the total number of reads

The third mechanism — speculative decoding — is particularly important. Speculative decoding works by having a lightweight draft model predict several tokens ahead, which the main model then verifies in a single batch. The benefit is greatest in low-concurrency environments with limited bandwidth.

DGX Spark fits this profile exactly: narrow bandwidth and low concurrency. DSpark is a method published by a team from Peking University and DeepSeek, and its name happens to look a lot like "DGX Spark." The official recommendation of DSpark for DGX Spark is therefore not just a name coincidence.

The three types of speculative decoding

Nemotron 3.5 Lightning comes bundled with three methods: MTP, DFlash, and DSpark. Here is an overview of all three.

Method Draft source Draft generation approach Separate checkpoint
MTP Built-in MTP layers Predicts multiple future tokens at each position Not required (built into the main model)
DFlash Separate draft model A block diffusion model generates one block in a single forward pass ...-NVFP4-DFlash
DSpark Separate draft model Semi-autoregressive; combines a parallel backbone with a lightweight sequential module ...-NVFP4-DSpark

The following Lightning-related checkpoints are published on Hugging Face.

Checkpoint Purpose
nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4 Main model (NVFP4)
nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16 Main model (BF16)
nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-Base-BF16 Base model
nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DSpark Draft model for DSpark
nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DFlash Draft model for DFlash

MTP (Multi-Token Prediction)

MTP is built directly into the main model during pre-training. In Nemotron 3.5 Lightning, a continued pre-training phase is used to train the MTP layers after standard pre-training, followed by a dedicated reinforcement phase to further improve MTP accuracy. The same technique is used in Nemotron 3 Super and Nemotron 3 Ultra.

The biggest advantage is that no separate draft model is needed. The official blog describes it as optimal for medium to high concurrency, and notes that the optimal draft length decreases as concurrency increases.

DFlash

DFlash is a speculative decoding method developed at UC San Diego's Z-lab. NVIDIA's official blog published an entry showing up to 15x inference performance improvement on NVIDIA Blackwell using DFlash speculative decoding. Since DGX Spark is also a Blackwell family device, solid performance gains can reasonably be expected.

The official blog describes it as potentially delivering the best performance for certain workloads compared to other models.

DSpark

DSpark is a method published on arXiv in July 2026. The paper is titled "DSpark: Confidence-Scheduled Speculative Decoding with Semi-Autoregressive Generation."

DSpark is an extension of DFlash. As a real-world production result, when integrated into DeepSeek-V4's serving environment, it reportedly improved per-user generation speed by 60–85% compared to the production baseline of MTP-1 at the same throughput level.

In the Nemotron 3.5 Lightning model card, DSpark is positioned as recommended for DGX Spark and low-concurrency datacenter workloads, with the note that DSpark is currently recommended for all cases.

Let's try it

Prerequisites

Item Details
Hardware NVIDIA DGX Spark (GB10 / unified memory 128GB / 273 GB/s)
Inference engine vLLM (Docker image vllm/vllm-openai:v0.27.1)
Main model nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4
Draft models nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DSpark / nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DFlash
Measurement conditions Concurrency 1 and concurrency 8, code generation tasks only

Setting up Docker

vLLM runs inside a container. For the DGX Spark setup, I followed the official vLLM Playbook, and for launching the container, I followed the Nemotron 3.5 Lightning vLLM cookbook.

The container image used is vllm/vllm-openai:v0.27.1, as specified in the Nemotron 3.5 Lightning model card.

$ docker pull vllm/vllm-openai:v0.27.1

Next, start the container. Following the vLLM cookbook, I use --entrypoint /bin/bash to enter the shell and then run vllm serve from inside the container.

$ docker run --rm -it --gpus all --ipc=host --network=host \
    -v ~/.cache/huggingface:/root/.cache/huggingface \
    --entrypoint /bin/bash \
    vllm/vllm-openai:v0.27.1

Unless otherwise noted, all subsequent commands are run inside this container. First, set the models to use as environment variables.

# Inside container
$ export MODEL_CKPT=nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4
$ export DSPARK_CKPT=nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DSpark
$ export DFLASH_CKPT=nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4-DFlash

Basing on the model card's DGX Spark recipe

The vllm serve options are based on the "1x DGX Spark (GB10)" recipe from the model card.

Speculative Decoding Strategies

The original looks like this.

# Model card "1x DGX Spark (GB10)" / Specdec method - DSpark
$ vllm serve --model $MODEL_CKPT \
    --moe-backend marlin \
    --kv-cache-dtype fp8 \
    --max-model-len 1048576 \
    --enable-prefix-caching \
    --gpu-memory-utilization 0.91 \
    --speculative_config.num_speculative_tokens 3 \
    --mamba-backend flashinfer \
    --mamba-cache-mode align \
    --reasoning-parser nemotron_v3 \
    --speculative_config.model $DSPARK_CKPT \
    --tool-call-parser qwen3_coder \
    --enable-auto-tool-choice

I swap out the --speculative_config.* portion of this recipe to create four configurations and measure token throughput.

Configuration --speculative_config.* specification
Baseline Both lines removed
MTP .method mtp + .num_speculative_tokens 3 + .moe_backend triton
DFlash .method dflash + .model $DFLASH_CKPT + .num_speculative_tokens 3
DSpark .method dspark + .model $DSPARK_CKPT + .num_speculative_tokens 3

--speculative_config.method does not appear in the original model card text — the draft model specification alone is sufficient to make it work. However, since MTP has no draft model, omitting method would make it indistinguishable. To keep the syntax consistent across all four configurations, I chose to explicitly specify method for DFlash and DSpark as well, following the cookbook.

Changes made

I changed --max-model-len from the baseline. While the model card specifies 1M tokens (1048576), I lowered it to 65536 across all configurations.

This provides more KV cache headroom during the concurrency-8 measurements and ensures consistent comparison conditions across all four configurations. The model card itself notes the following for the H100 and GB200 recipes, indicating that lowering the value to match the workload is expected:

If you're memory-constrained — or want more KV-cache headroom at high concurrency — lower --max-model-len to match your workload.

Baseline (no speculative decoding)

First, start the plain configuration to serve as the comparison baseline.

# Inside container
$ vllm serve --model ${MODEL_CKPT} \
    --moe-backend marlin \
    --kv-cache-dtype fp8 \
    --max-model-len 65536 \
    --enable-prefix-caching \
    --gpu-memory-utilization 0.91 \
    --mamba-backend flashinfer \
    --mamba-cache-mode align \
    --reasoning-parser nemotron_v3 \
    --tool-call-parser qwen3_coder \
    --enable-auto-tool-choice

Loading the 30B model takes a few minutes. Following the cookbook, wait until /v1/models responds. Run this in a separate terminal on the host side.

# Host side
$ until curl -sf http://localhost:8000/v1/models > /dev/null 2>&1; do
    echo "Waiting for server..."; sleep 5
  done
  echo "Server is ready"
Waiting for server...
Waiting for server...
Waiting for server...
Waiting for server...
Waiting for server...
Server is ready

The first run will take longer as the model needs to be downloaded.

MTP

Since MTP is built into the main model, no separate draft model is needed. It is activated by adding --speculative_config.method mtp, --speculative_config.num_speculative_tokens 3, and --speculative_config.moe_backend triton. Initially I omitted --speculative_config.moe_backend, which caused an error because --moe-backend marlin is not supported by the drafter. --speculative_config.moe_backend must be one of triton, batched_triton, flashinfer_trtllm, flashinfer_cutlass, or aiter.

# Inside container
$ vllm serve --model ${MODEL_CKPT} \
    --moe-backend marlin \
    --kv-cache-dtype fp8 \
    --max-model-len 65536 \
    --enable-prefix-caching \
    --gpu-memory-utilization 0.91 \
    --speculative_config.method mtp \
    --speculative_config.num_speculative_tokens 3 \
    --speculative_config.moe_backend triton \
    --mamba-backend flashinfer \
    --mamba-cache-mode align \
    --reasoning-parser nemotron_v3 \
    --tool-call-parser qwen3_coder \
    --enable-auto-tool-choice

DFlash

DFlash requires specifying a separate draft model.

# Inside container
$ vllm serve --model ${MODEL_CKPT} \
    --moe-backend marlin \
    --kv-cache-dtype fp8 \
    --max-model-len 65536 \
    --enable-prefix-caching \
    --gpu-memory-utilization 0.91 \
    --speculative_config.method dflash \
    --speculative_config.model ${DFLASH_CKPT} \
    --speculative_config.num_speculative_tokens 3 \
    --mamba-backend flashinfer \
    --mamba-cache-mode align \
    --reasoning-parser nemotron_v3 \
    --tool-call-parser qwen3_coder \
    --enable-auto-tool-choice

DSpark

DSpark similarly requires specifying a draft model.

# Inside container
$ vllm serve --model ${MODEL_CKPT} \
    --moe-backend marlin \
    --kv-cache-dtype fp8 \
    --max-model-len 65536 \
    --enable-prefix-caching \
    --gpu-memory-utilization 0.91 \
    --speculative_config.method dspark \
    --speculative_config.model ${DSPARK_CKPT} \
    --speculative_config.num_speculative_tokens 3 \
    --mamba-backend flashinfer \
    --mamba-cache-mode align \
    --reasoning-parser nemotron_v3 \
    --tool-call-parser qwen3_coder \
    --enable-auto-tool-choice

Note that the DGX Spark recipe in the model card does not include --speculative_config.method, and is written to work with only the draft model specified. Since the cookbook side explicitly specifies method, I aligned with the cookbook and made it explicit here as well.

After all measurements are complete, stop vLLM with Ctrl-C and then exit the container with exit. Since the container was started with --rm, it will be automatically deleted.

Running the benchmark

The client follows the cookbook's approach. The cookbook uses openai==2.38.0, with base_url set to http://127.0.0.1:8000/v1 and api_key set to null. Sampling settings are also fixed to the officially recommended Temperature 1.0 / Top_P 0.95.

$ pip install openai==2.38.0

I prepared a benchmarking script as bench.py. It sends code generation task prompts at a specified concurrency level and aggregates throughput.

import argparse
import time
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI

PROMPTS = [
    "Write a Python function that parses an nginx access log file and returns the top 10 IP addresses by request count. Include type hints and docstrings.",
    "Implement a thread-safe LRU cache in Python with get and put in O(1). Include type hints and docstrings.",
    "Write a Python script that walks a directory tree and reports the 20 largest files, with a --min-size option. Include type hints and docstrings.",
    "Implement binary search over a rotated sorted array in Python, handling duplicates. Include type hints and docstrings.",
]

def parse_args():
    p = argparse.ArgumentParser()
    p.add_argument("--base-url", default="http://127.0.0.1:8000/v1")
    p.add_argument("--model", default="nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4")
    p.add_argument("--concurrency", type=int, default=1)
    p.add_argument("--requests", type=int, default=8)
    p.add_argument("--max-tokens", type=int, default=2048)
    p.add_argument("--warmup", type=int, default=2)
    return p.parse_args()

def one_request(client, args, index):
    prompt = PROMPTS[index % len(PROMPTS)]
    start = time.perf_counter()
    response = client.chat.completions.create(
        model=args.model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=args.max_tokens,
        temperature=1.0,
        top_p=0.95,
        extra_body={"chat_template_kwargs": {"enable_thinking": False}},
    )
    elapsed = time.perf_counter() - start
    return response.usage.completion_tokens, elapsed

def main():
    args = parse_args()
    client = OpenAI(base_url=args.base_url, api_key="null")

    for i in range(args.warmup):
        one_request(client, args, i)

    wall_start = time.perf_counter()
    with ThreadPoolExecutor(max_workers=args.concurrency) as executor:
        results = list(executor.map(lambda i: one_request(client, args, i), range(args.requests)))
    wall = time.perf_counter() - wall_start

    total_tokens = sum(tokens for tokens, _ in results)
    per_request_tps = [tokens / elapsed for tokens, elapsed in results]

    print(f"concurrency     : {args.concurrency}")
    print(f"requests        : {args.requests}")
    print(f"wall clock      : {wall:.2f} s")
    print(f"total tokens    : {total_tokens}")
    print(f"aggregate TPS   : {total_tokens / wall:.2f} tok/s")
    print(f"per-request TPS : {sum(per_request_tps) / len(per_request_tps):.2f} tok/s (avg)")
    print(f"tokens/request  : {total_tokens / len(results):.1f} (avg)")

if __name__ == "__main__":
    main()

ThreadPoolExecutor(max_workers=args.concurrency) limits the number of simultaneous requests, and the wall-clock time to complete all --requests is measured. Concurrency 1 and concurrency 8 are run as follows.

$ python3 bench.py --concurrency 1 --requests 10
concurrency     : 1
requests        : 10
wall clock      : 145.77 s
total tokens    : 11853
aggregate TPS   : 81.31 tok/s
per-request TPS : 81.27 tok/s (avg)
tokens/request  : 1185.3 (avg)
$ python3 bench.py --concurrency 8 --requests 80
concurrency     : 8
requests        : 80
wall clock      : 406.28 s
total tokens    : 98185
aggregate TPS   : 241.67 tok/s
per-request TPS : 30.97 tok/s (avg)
tokens/request  : 1227.3 (avg)

At concurrency 1, I use per-request TPS as the perceived speed for a single user; at concurrency 8, I use aggregate TPS as the overall server throughput.

Notes on measurement conditions

I made three adjustments when writing the script.

  • enable_thinking is set to False.
    • Nemotron 3.5 Lightning has reasoning (thinking) enabled by default, but for the agent execution layer use case assumed here — high-frequency steps like tool calls and result formatting — disabling thinking results in higher speed.
    • It also stabilizes the output token count for a fair comparison of speculative decoding methods.
  • Four different prompts are cycled through.
    • Sending identical prompts would cause prefill cache hits, so multiple different prompts are used to reduce the likelihood of caching.
  • Two warmup requests are included.
    • The first few requests after startup are slower due to CUDA graph captures and similar initialization, so the --warmup requests are excluded from measurement.

Measurement results

| Method | Concurrency 1
per-request TPS | Concurrency 8
aggregate TPS | vs. Baseline
(concurrency 1) | vs. Baseline
(concurrency 8) |
|---|---|---|---|---| 
| No speculative decoding | 81.27 | 241.67 | 100.00% | 100.00% |
| MTP | 111.36 | 302.27 | 137.02% | 125.08% |
| DFlash | 95.48 | 268.61 | 117.48% | 111.15% |
| DSpark | 124.24 | 354.56 | 152.87% | 146.71% |

Token throughput with speculative decoding

Speculative decoding clearly has a positive effect, with DSpark showing the highest gains at both concurrency 1 and concurrency 8. While the documentation suggests MTP becomes more effective at higher concurrency, the ranking did not change at concurrency 8 on DGX Spark. In these measurements, DFlash came in last among the speculative decoding methods, though this may vary depending on the workload.

Closing thoughts

Having three types of speculative decoding bundled with the same model, along with hardware-specific recommendations for each, is a configuration I haven't seen before. Rather than simply being a fast model, the fact that it specifies which method to use on which hardware and why reflects the character of Nemotron 3.5 Lightning as a model.

What I personally found interesting in this evaluation is that the 273 GB/s memory bandwidth constraint of the DGX Spark seems to connect directly to the design philosophy of Lightning. Because it targets always-on AI agents, local deployment is part of the picture, and the design makes it possible for a 30B-class model to run at practical speeds on bandwidth-limited hardware like the DGX Spark.

Getting a DGX Spark as an individual still has a high barrier, but having an always-on agent running on local hardware is becoming an increasingly realistic scenario. The requirement to run agents without sending data outside — especially prevalent among manufacturing customers — remains strong, and I expect progress in this direction to continue.

If you already have a DGX Spark, I recommend starting with the officially recommended DSpark configuration. The playbook hasn't fully caught up yet, so there is some manual setup involved, but the model card recipe works as-is, so using that is the safest approach.


AI白書2026 配布中

クラスメソッドが独自に行なったAI診断調査をもとに、企業のAI活用の現在地を調査レポートとしてまとめました。企業規模別の活用度傾向に加え、規模を超えてAI活用を進める企業に共通する取り組みまで、自社の現在地を捉えるためのヒントにぜひ。

AI白書2026

無料でダウンロードする

Share this article