Ollama on the ASUS Ascent GX10 (GB10): Real Tokens per Second for Qwen 27B, a 35B MoE and gpt-oss-20b

I run local models around the clock on an ASUS Ascent GX10, the ASUS version of NVIDIA's DGX Spark: a GB10 Grace Blackwell chip with 128 GB of memory shared between CPU and GPU. Before you buy one (or while deciding which model to keep loaded), you want to know one thing: how fast does it actually generate text?

Here are my measured numbers with Ollama for three models I actually use, plus the quirks of this machine that tripped me up: nvidia-smi can't show memory use, the first request after a model loads costs several seconds, and repeated prompts look impossibly fast.

The setup

  • ASUS Ascent GX10, NVIDIA GB10, 128 GB unified memory (Linux reports about 121 GiB)
  • Driver 580.178, CUDA 13.0, Ollama 0.40.3
  • All models in Ollama's default 4-bit quantization (Q4_K_M; gpt-oss uses its native MXFP4)
  • Thinking turned off for the Qwen models ("think": false), so the numbers measure the answer, not a hidden reasoning trace

Generation speed

Same prompt for every model (a 200-word explanation of a four-bar linkage), 300 output tokens, temperature 0, each model run twice: once right after loading from disk (cold) and once already in memory (warm). Nothing else was using the GPU.

ModelTypeFile sizeGenerationLoad time300 tokens, warm
qwen3.6:35b-a3bMoE, ~3B active23.9 GB75 tok/s6.9 s3.6 s
gpt-oss:20bMoE13.8 GB61 tok/s4.2 s5.0 s
qwen3.8:27bdense17.7 GB26 tok/s6.4 s11.5 s

The generation speed was identical between the cold and warm runs (within 0.5 tok/s). Only the load time differs.

The pattern is clear: mixture-of-experts models are about three times faster than the dense 27B here. Token generation is mostly limited by how fast the chip can read the model's weights from memory, and a MoE model only reads the few "experts" it uses for each token. The 35B MoE needs more memory than the 27B dense model, yet it answers about three times sooner.

What 26 tok/s feels like: a short reply (one or two sentences) arrives in 1–3 seconds. A 300-token answer takes about 11 seconds. Fine for background jobs and summaries, a bit slow for chatting. 75 tok/s feels close to a cloud service.

Reading long prompts

For summarizing documents, prompt processing matters more than generation. I fed each model the same ~4,200-token Markdown file:

ModelPrompt processing, first timeTime to read 4,200 tokens
gpt-oss:20b~3,800 tok/s~1.1 s
qwen3.6:35b-a3b~2,250 tok/s~1.9 s
qwen3.8:27b~810 tok/s~5.2 s

The first-time column includes some one-off setup, so treat it as a lower bound.

Watch out for prompt caching when you benchmark. Sending the exact same long prompt a second time, Ollama reported 30,000 to 219,000 tok/s. That isn't real speed: Ollama keeps the processed prompt in memory and only processes what's new. Great for chat (the conversation history isn't re-read every turn), misleading for benchmarks. Change the prompt between runs, or only trust the first run.

Quirk 1: nvidia-smi can't show memory use

On the GB10, the GPU has no separate VRAM, so nvidia-smi shows Memory-Usage: Not Supported and --query-gpu=memory.used returns [N/A]. To see what's loaded and how much it takes, ask Ollama:

$ ollama ps
NAME           ID              SIZE     PROCESSOR    CONTEXT    UNTIL
qwen3.8:27b    22130167c4c2    18 GB    100% GPU     262144     29 minutes from now

The 18 GB includes the context cache. On a regular PC, a 27B model at 4-bit wouldn't fit in a 16 GB card. Here it uses about a seventh of the memory, which is why I can keep a chat model, an embedding model and a vision model loaded at the same time (the embedding model powers my local notes search). free -g also works, since GPU memory is just system memory.

Quirk 2: load time is the real latency

Ollama unloads a model after 5 minutes of inactivity by default. The next request then waits 4–9 seconds for the model to load from disk before the first token. For a script that fires once an hour, that load time is most of the wait. You can control it per request with keep_alive:

# keep a model loaded for 2 hours after each request
curl -s localhost:11434/api/generate -d '{"model":"qwen3.8:27b","keep_alive":"2h"}'

# unload it right now (frees the memory for something else)
curl -s localhost:11434/api/generate -d '{"model":"qwen3.8:27b","keep_alive":0}'

(Or set OLLAMA_KEEP_ALIVE for the Ollama service to change the default.) With 128 GB, keeping one or two models resident is cheap. If the machine is shared, unload big models when you're done.

What I use each model for

  • qwen3.8:27b: my default for summaries, format conversions and drafts in Chinese and English. It's the slowest of the three, but for background jobs nobody is waiting on, I'd rather have the bigger dense model.
  • qwen3.6:35b-a3b: the one to pick when speed matters, for example processing many documents in a loop.
  • gpt-oss:20b: the fastest at reading long input. A good fit for "read this log and tell me what failed".

Anything that affects money or needs real judgment still goes to a frontier model. The local models handle the high-volume, low-risk work for free, and nothing leaves the machine. The same box also runs a 24/7 Claude Code remote session.

Run the test yourself

The script I used. It loads each model, runs the prompt twice, and unloads it:

#!/usr/bin/env python3
"""Rough Ollama speed test: cold start (model loaded from disk) vs warm, same prompt.
Usage: python3 ollama-bench.py qwen3.8:27b gpt-oss:20b ..."""
import json, sys, time, urllib.request

URL = "http://127.0.0.1:11434/api/generate"
PROMPT = "Explain in about 200 words how a four-bar linkage converts rotary motion into oscillating motion."


def post(body):
    req = urllib.request.Request(URL, json.dumps(body).encode(), {"Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req, timeout=900))


def run(model):
    body = {"model": model, "prompt": PROMPT, "stream": False,
            "options": {"num_predict": 300, "temperature": 0, "seed": 1}}
    if model.startswith("qwen"):
        body["think"] = False          # measure the answer, not a long reasoning trace
    t = time.time()
    r = post(body)
    return {"wall_s": round(time.time() - t, 1),
            "load_s": round(r.get("load_duration", 0) / 1e9, 1),
            "gen_tokens": r["eval_count"],
            "gen_tok_per_s": round(r["eval_count"] / (r["eval_duration"] / 1e9), 1)}


for m in sys.argv[1:]:
    print(m, "cold", run(m), flush=True)
    print(m, "warm", run(m), flush=True)
    post({"model": m, "keep_alive": 0})   # unload before testing the next model

One prompt and two runs per model is a rough test, not a formal benchmark. Your numbers will move with prompt length, context size and quantization, but the ranking (MoE far ahead of dense on generation) held in every run I did.

Comments

Popular posts from this blog

Gear Ratio Calculator: Multi-Stage Ratio, Output Speed, Torque and Direction

Mecanum-Wheel Pick-and-Place Robot: From Arduino UNO to Mega2560 + ESP32-CAM

Voltage Divider Calculator: Output Voltage, or the Best E24 Resistor Pair for a Target Voltage