Benchmark report · revised 2026-09-23

Devstral and three other local models on τ-bench retail

Can devstral-cuda run a multi-turn, tool-calling agent loop on this hardware, and how does it compare to the rest of the shelf?

115 retail tasks 4 models 2× P100 16 GB scripted customer run 2026-09-22/23

What the run shows

Tool calling is not where these models fail

Zero tool-call parse failures in about 4,000 model calls over 375 completed tasks, across four architectures (mistral3, gpt-oss, qwen35, g9v3). llama.cpp's --jinja parsers handled every format. Every failure was reasoning or policy behaviour.

The scripted customer penalises asking, but can't reverse the ranking

A model that asks a question never gets an answer and scores zero. Half of gpt-oss's stalls never found the order they were meant to act on. With those removed, the stall problem can at most tie gpt-oss with devstral (57.4% each), not put it ahead.

Every passing write broke the task policy

The retail policy requires an explicit “yes” before any write. The scripted customer never says it: 0 of 560 writes were confirmed. Devstral repeated identical writes 24 times across 14 tasks.

Results

ModelnPassStall:
lookup
Stall:
asked
CeilingUnterm.ErrParse
fail
Hours
qwen3.6-27b-abliteratedq5_k_m11576 · 66.1%2671.3%9807.80
g9v3-39a5bq4_k_m · partial3017 · 56.7%0986.7%0001.09
devstral-patchedq8_011561 · 53.0%0557.4%3003.45
gpt-oss-20bq8_011552 · 45.2%161457.4%0001.52
Stall
A failed, completed, error-free task whose ground truth contains a write, where the agent made none.
Stall: lookup
The agent never opened any order the ground truth acts on. It would have failed whatever the customer said: a model failure.
Stall: asked
Found the right order, then ended on a question. The only stall the scripted customer can be blamed for.
Ceiling
(pass + asked stalls) ÷ n. An upper bound: assumes every answered question leads to a correct write.

gpt-oss and qwen3.6 each had one more stall that found the order and stopped silently.

Where each model's tasks went

Each bar is one model's tasks, as a share of the tasks it ran. The blue and amber segments together make up its ceiling.

The stall confound

Real τ-bench drives the customer with a frontier LLM. There is no metered API key on this box, and 24–28 GB of agent weights leave no VRAM for a second local model, so the customer is scripted. The full instruction arrives on turn 1, and every later turn is a fixed nudge that adds nothing. These numbers are not comparable to published τ-bench results. The harness is easier for some models than for others.

A scripted customer cannot answer a question. A model that asks “Option A or B? Confirm the payment method?” never gets an answer and scores zero. But not every stall is a model waiting on the customer, so each one is checked against the ground truth: did the agent call get_order_details on any order the correct writes touch?

Every failure, by cause

devstralgpt-ossqwen3.6g9v3 (30)
Stall: found order, asked (harness)51469
Stall: never opened target order01620
Stall: found order, stopped silently0110
Acted, wrong arguments3622193
Acted, required output missed101021
Unterminated3010
Error (TimeoutError)0080
Total failures54633913
Harness-attributable share9.3%22.2%15.4%69.2%

The “asked” test is run.py's ASKING regex, which matches any final message ending in a question. It is generous, so the ceilings are upper bounds twice over. Stall totals match the earlier draft; only the split changed.

Two conclusions survive

g9v3's 69.2% harness share is the largest here. On 30 tasks, its 86.7% ceiling is the least reliable number in the report.

The policy requires a “yes” the harness never gives

The retail system prompt says:

Before taking consequential actions that update the database (cancel, modify, return, exchange), you have to list the action detail and obtain explicit user confirmation (yes) to proceed.

Neither customer policy ever says yes. The turn-1 restatement ends “Please go ahead and complete it”, a blanket go-ahead given before the agent has listed any action. That doesn't satisfy the policy as written.

devstralgpt-ossqwen3.6g9v3 (30)
Writes issued25012515629
…without a prior “yes”25012515629
Passes containing an unconfirmed write56 / 6144 / 5272 / 7613 / 17
Writes beyond the ground truth8710161
Identical writes repeated24 in 14 tasks02 in 1 task0

The remaining passes are tasks that needed no write. Under this harness, no model can pass a write task while following its instructions. A model that lists the action and asks for a yes is complying, and scores zero for it.

This doesn't separate the models, because every write is unconfirmed by design. What it changes is how devstral's low stall rate reads: it takes the blanket go-ahead as enough and doesn't follow the confirmation rule in its own system prompt.

For an AIOS-style planner: a “confirm before writing” rule in the prompt can't be relied on to hold devstral back. The check has to be enforced in the control plane: the tool layer refuses writes that carry no confirmation token. Once a real user simulator exists, the unconfirmed-writes count becomes a per-model policy-violation rate.

Attempted fix: the confirm customer policy

run.py --user-policy confirm answers a confirmation request and restates the instruction, instead of the default nudge, which adds nothing. A/B test on devstral, tasks 40–63:

nudgeconfirm
Pass14 / 2415 / 24
Stalled50
Mean steps10.315.7
Time per task98 s147 s (1.49×)

Gained 40, 45, 55 and 58; lost 56, 60 and 62. Net +1 on 24 tasks, and a net loss on tasks 51–63 (7/13 → 6/13): within noise, at 1.5× the time. Stalls went to zero, but an over-action problem replaced them. Task 62's correct behaviour is to change nothing, yet under confirm devstral issued a spurious cancel_pending_order. Rewording the reply to refuse any wider mandate made it worse: two spurious writes instead of one. What drives this is the extra turn itself, not its wording.

nudge stays the default. The real fix is an LLM customer: a canned reply can't tell “yes, do what you asked about” from “no, that's outside my request”. gemma4-e4b (6.0 GB) fits beside devstral (23.9 GB) in 32 GB, with about 2.8 GB left for both KV caches. It's feasible but not built.

Failure modes seen

  1. Miscounting over tool output. Asked how many T-shirt options are available, devstral said 11. The data holds 12 variants, 10 of them available: true. It called get_product_details correctly and then counted wrong: an aggregation error, not a tool error.
  2. Duplicate writes. Devstral re-issued a write with identical arguments 24 times across 14 tasks (2, 5, 11, 20, 22, 34, 49, 52, 53, 58, 75, 78, 91, 97), not recognising that the first call had succeeded. It was mostly harmless here only because the retail backend checks order state. It rejected 20 of the 24, and the other 4 were identical modify_user_address rewrites. Five of the 14 tasks still passed. A backend without those checks would have executed every repeat. This is the most concerning failure for a control plane, and an argument for idempotency keys on every write tool.
  3. Wrong variant selection. Right tool, right order, wrong new_item_ids.
  4. Stalls. Covered above: some ask for a confirmation nobody gives, and others ask for information the model never looked up.

17 tasks were solved by no model. Their required outputs are mostly long numbers (746342064230, 1288.65, 46.66), so reporting exact values is harder here than sequencing tool calls.

Cost

ModelWall clockvs devstral
gpt-oss-20b1.52 h0.44×
devstral3.45 h1.00×
qwen3.6-27b7.80 h2.26×

qwen3.6 gains 13 points over devstral for 2.3× the wall clock, 3× the unterminated tasks and 8 timeouts. gpt-oss is 2.3× faster than devstral, ties it at the most generous reading of the stall problem, and never repeated a write.

Setup, caveats and reproducing

Setup
  • Router: llama.cpp, CUDA 12.6 build for sm_60 (Pascal), --jinja, ctx 32768, f16 KV cache, --models-max 1, so one model is loaded at a time.
  • The f16 KV cache is required, not a preference. The g9v3 fork's graph aborts on a quantized cache (GGML_ASSERT(obj_new) in build_arch_graph), so using f16 everywhere put all four models on one config. g9v3 also needs the fork build at ~/llama-cuda12-g9v3.
  • Both Qwen models ran with enable_thinking: false, confirmed by empty reasoning_content and completion tokens halved (98→52, 95→52). gpt-oss reasons through its built-in harmony format and can't be cleanly switched off; devstral doesn't reason.
  • Scoring is τ-bench's own calculate_reward(), unmodified: the ground-truth actions are replayed on a fresh database, the database states are compared, and required output strings are checked. No partial credit.
Caveats
  • g9v3 ran 30 of 115 tasks, stopped deliberately while its early results looked structurally bad. It's a partial sample and can't be ranked directly against the others.
  • qwen3.6 lost 8 tasks to TimeoutError, all scored as failures, so its true rate is somewhat above 66.1%.
  • The customer is scripted, as above. The .PARTIAL-superseded-by-unsloth files come from the abandoned uncensored-Q6_K_P run and contain no results.
  • qwen3.8-27b-unsloth_q8_0 was pulled from this run to be scheduled separately; drive_unsloth.sh is staged but hasn't been run.
Reproducing
cd ~/Projects/Tests/2026-09-22/devstral-tau-scripted
./drive_all.sh          # devstral, gpt-oss, qwen3.6, g9v3 -- resumable
./drive_unsloth.sh      # qwen3.8 unsloth q8_0, standalone
../../.venv/bin/python summarise.py

A model is skipped if its JSON already holds 115 results, so an interrupted run resumes at the next model. summarise.py runs on the CPU only and prints every number here except the A/B test.