mudler / mudler/vllm.cpp

TT eager decode spends the wall on host staging, not device kernels: cache resolved handles, bulk the EnsureDevice2D element loop, batch per-layer staging

Open
#2,107 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
423
Forks
53
Avg merge
20h 26m
Merged PRs (30d)
310

Description

Row: BACKEND-TENSTORRENT-QWEN35

Owning row: BACKEND-TENSTORRENT-QWEN35. Follow-up to the #1715 profile lever, which measured the wall before naming a fix.

Measured (2026-08-27, P150, tree a0db99b31, docs/bench-evidence/tt-qwen35-eager-profile-20260827.log)

One eager Qwen3.5-0.8B decode step runs at 0.104 tok/s and the profile names no device kernel in any ranked frame. The wall is host dispatch around the TT GEMM:

  • vt::Tensor::Numel() 27.09% of samples — per-element shape math inside the staging loop
  • EnsureDevice2DMatmulBTKernel = 24.10% of the call graph, under MatmulBf16D 19.33%, fed by DenseMlpBlock 9.44% and DenseLogitsF32D 9.32%
  • repeated TT-Metal context/UMD discovery ~12% (MetalContext::instance 5.81%, Cluster::get_chip 3.27%, get_closest_mmio_capable_chip 2.95%, DeviceManager::get_active_device 2.49%)
  • CPU threadpool spin 11.4%, memcpy 7.04%, bfloat16::from_float 2.62%
  • Control: the same leg without the TT backend runs at 7.521 tok/s (~73x faster)

Root cause in code

EnsureDevice2D (src/vt/tenstorrent/tenstorrent_ops.cpp:434) stages element-by-element through an f32 intermediate:

std::vector<float> host(rows * cols);
for (int64_t i = 0; i < t.Numel(); ++i)
  host[i] = LoadElemF32(t, i);   // per-element stride/dtype dispatch
ttnn::Tensor dev = UploadRows(host.data(), rows, cols, device);  // f32->bf16 again

plus up to four FindSlot mutex acquisitions per call, and every upload path re-resolves TT-Metal context/device/chip handles.

The three levers (in the recorded next gate)

  1. Cache resolved context/device/chip handles across calls.
  2. Hoist Numel()/shape math out of per-call staging; bulk the element loop.
  3. Batch per-layer staging.

Constraints

  • Numerics may not move: the sacred golden pair must stay 16/16 (tests/parity/goldens/qwen35_greedy_0_8b), full TT suite green.
  • bf16 host masters must not round-trip through f32 where a bulk copy suffices; f32 stays where the model path declares it (logits GEMM f32 output is the annotated exception).
  • Captured tracing stays blocked behind #1625; this is eager-side only.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/vt/tenstorrent/tenstorrent_ops.cpp at EnsureDevice2D (line 434), then inspect its callers and the TT upload paths that resolve context, device, and chip handles. Use docs/bench-evidence/tt-qwen35-eager-profile-20260827.log and the #1715 profile as the baseline. Done means the recorded caching, bulk staging, and per-layer batching work is covered by the full TT suite while tests/parity/goldens/qwen35_greedy_0_8b remains 16/16.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend, performance
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.