DRAM sharding config for matmuls in LLMs #5972
Replies: 4 comments
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
|
before adding guard to avoid transforming shape-inadequate matmul - DRAM-sharded matmul on p150: slower on every model measured: https://claude.ai/code/artifact/01907768-d057-400c-bb87-632afee36389 after adding guard - Declining the collapsed block width removes the Blackhole regression: https://claude.ai/code/artifact/c9c23343-c87d-4f75-a287-8496df6201f2 lofi located as the compute-bound reason Blackhole doesn't progress: https://claude.ai/code/artifact/7772cbbf-c9f3-4221-adb6-1237cd86cb15 Forcing LoFi speeds up p150, costs no significant correctness measured locally with top1/top5 on n150/p150: https://claude.ai/code/artifact/cf9ca218-4477-4c47-9279-35d717c3bd1a |
Uh oh!
There was an error while loading. Please reload this page.
DRAM-sharded matmul (tt-mlir PR #9150) — benchmark analysis and root cause
Analysis of the DRAM-sharded (DS) matmul optimizer integration across four
platforms, the mechanism behind the Blackhole regressions, and why the obvious
fix to the
in0_block_wdivisor constraint is not viable.1. What was compared
All eight runs are tt-xla
f6a38d65. The only variable is the tt-mlir pin.99c621daae602833bmalesevic/dram-sharding-matmul-optimizer-integration-rebased(PR #9150) — afterAll runs used
skip device perf: true, so every number below is end-to-endsamples/sec. There is no kernel-level attribution anywhere in this analysis.
2. Headline results
Split by whether the model's TTNN artifacts actually contain
#ttnn.matmul_multi_core_reuse_multi_cast_dram_sharded_program_config. Modelswith zero DS matmuls are the control group — same dispatch, same runner pool,
same tt-mlir swap, but the optimizer changed nothing about them.
Both control groups on n150/p150 centre on +0.4%, and 28 of 30 p150 controls sit
inside ±3%, so the affected-group signal is real and not machine drift. The one
control outlier is
mnist(−12.2% p150, −3.5% n150), which runs at 25–28ksamples/sec and is host-bound.
qb2's +0.70% is barely outside its own control band (±1.6%), so the honest read
is neutral, not a win.
Detail — n150 (+2.5%)
Detail — p150 (−6.5%)
Detail — qb2 (+0.70%)
3. What DS actually changes in the IR
Verified across 25 model × platform combinations: every "before" artifact
contains zero DS program configs. In the "after" artifacts DS appears in
exactly one graph per model — g1, the decode graph (g2 for gemma, whose
graph order differs). Prefill (g0) config mix is unchanged, and the DS counts are
identical between n150 and p150, so both platforms execute the same decision.
The vLLM path is untouched. All 34
vllm_*benchmarks show zero DS matmulsin every graph, before and after. This analysis says nothing about vLLM serving.
Role of each core group
From tt-metal
matmul_multicore_reuse_mcast_dram_sharded_program_factory.cpp:kNumIn0Cores = 8MatmulRules.cpp:41)num_worker_cores = num_dram_banksdiv_up(N_tiles, numOutCores)→ gridnumOutCores = worker grid volumeThe output is not capped at 8:
<1x48>/<1x56>on n150,<1x48>/<1x94>on p150. So 8–12 compute cores scatter results across up to 94 storagecores.
4. Two hard failures introduced
4a. n150 — DRAM OOM on the 7B–8B class
Four models produced a clean number before with zero OOM lines and die after;
a fifth already failed for another reason. Surfaces as
RuntimeError: Bad StatusOr access: INTERNAL: Error code: 13; artifacts stopafter g1.
Note free ≫ largest block: fragmentation, not only capacity. All five run fine on
p150, which has more DRAM.
Wormhole also pays a padding penalty the 8-bank parts don't:
padToDRAMBanks(8960, 12)rounds N up to a multiple of 384 → 9216 (2.9% waste);with 8 banks 8960 is already a multiple of 256.
4b. qb2 — L1 circular-buffer clash
qwen_3_32b_tp_qb2scored 16.57 sps before and fails after (artifacts stop at2 graphs):
A 1920-byte overshoot — with the conservative L1 reservation already in place.
The other five new qb2 failures are not DS-attributable (all have zero DS
matmuls):
glm_image,hunyuan_image_2_1,vllm_llama_3_1_70b_qb2_tptimedout;
vllm_qwen2_5_coder_32bhit a vLLM engine-init error;wan14b_vae_encoder_720p_shardedexited 3.5. Root cause:
in0_block_wcollapsecomputeShardParams(MatmulProgramConfig.cpp) startsin0BlockWatkPerCore = kTiles / kNumIn0Coresand walks it down to the largest divisorwhose circular buffers fit L1. Each halving of
in0_block_wdoublesnum_blocks = kTiles / in0_block_w, i.e. the number of serialized K-blocks withtheir mcast and CB-sync overhead.
Define ratio = achieved
in0_block_w/kPerCore. 1.00 means never walked down.in0_block_wat maxin0_block_wwalked downAll five n150 models keep
in0_block_wat max. 12 banks → narrower per-bankweight shard → smaller
in1CB → the budget never binds. That is a mechanicalexplanation of the n150/p150 split, and it supersedes the earlier "Blackhole has
worse per-bank bandwidth" hypothesis, which was speculation.
4 of 17 are misclassified (
p150 llama_3_2_1b−6.7% andqb2 llama_3_1_8b−1.5% lose without a walk-down;
qb2 llama_3_1_70bandcoder_32bwin with one— both very large, heavily DRAM-bound models). So a residual effect remains
unexplained.
The −28.1% case in full
p150 qwen_2_5_3b, decode graph:kTiles = 11008/32 = 344,kPerCore = 344/8 = 43, and 43 is prime, so thekPerCore % in0BlockW == 0constraint admits only{43, 1}. Blackholel1Available = 0.95 × (1572864 − 98304) = 1,400,832:So that projection runs 344 serialized K-blocks instead of 8.
in1CBis 83%of the requirement. This reproduces the emitted
ibw=1exactly.The qb2 pair that disagrees at identical shard width
llama_3_1_70b_tp+4.0%:ibw=16, m=1, n=3ibw=32, m=1, n=1ibw=8, m=1, n=3ibw=7, m=1, n=3mistral_small_24b_tp−3.5%:ibw=5, m=1, n=3ibw=20, m=1, n=1ibw=4, m=1, n=2ibw=16, m=1, n=2Why tensor parallelism rescues Blackhole
TP splits the weight across devices, shrinking the per-bank shard and therefore
in1CB. Same model, same 8 banks, decode graph:6. Why relaxing the
in0_block_wdivisor constraint is NOT viableThe tempting fix — let
in0_block_wtake values that don't dividekPerCore, soK=11008gets options between 43 and 1 — breaks tt-metal. Two independentreasons.
6a. A hard
TT_FATALonKt % in0_block_wmatmul_multicore_reuse_mcast_dram_sharded_program_factory.cpp:1012:Uncatchable abort. Any
in0_block_wnot dividingKtis rejected outright.6b. The kernel assumes a uniform, exact block→sender partition
Host (same file):
Both are integer division with no exactness check. The only
TT_FATALonnum_blocks_per_shardis guarded byif (per_core_M > 1), and DS requiresper_core_M == 1, so it never fires here.num_blocks_per_shardis then a constexpr compile-time arg toreader_bmm_tile_layout_in0_sender_dram_sharded.cpp, which derives:If
num_blocksis not an exact multiple of the sender count, three things gowrong at once:
num_storage_coresis recomputed wrong, so the mcast y-coordinate arrayis read from the wrong
get_arg_addroffset → mcast to garbage NoC coords.n_senders × nbpsblocks; the tail blocksare never sent, so part of K is never accumulated.
num_blocksand computesblock_id = block / nbps≥n_sendersfor the tail, indexing past the senderarray and waiting on a semaphore no one will signal → hang.
Wrong numerics and/or deadlock — never a clean error.
6c. tt-mlir's constraint is exactly right
Let
Kt= K in tiles,nc = kNumIn0Cores,q = Kt/nc(integer, guaranteed bythe eligibility gate
Kt % nc == 0).ibw | q.ibw | Ktand (b)nc | (Kt/ibw).⇒ If
ibw | q:Kt = nc·qsoKt/ibw = nc·(q/ibw)— both (a) and (b) hold.⇐ If (a) and (b):
Kt/ibw = nc·m→Kt = nc·m·ibw→q = m·ibw→ibw | q.The conditions are equivalent.
kPerCore % in0BlockW == 0is the exactnecessary and sufficient condition, not a conservative tt-mlir invention.
For
Kt=344, nc=8the legal set really is{1, 43}.6d.
in0_last_ktile_wis unrelatedIt is
K_elements % 32— the count of valid columns in the final K tile whenK is not tile-aligned, consumed by
pad_last_ktile<>()to zero the invalidcolumns. It has nothing to do with block granularity and offers no path to
partial blocks. (An earlier guess that it might enable a remainder block was
wrong.)
7. Recommendations
Ordered by value and confidence.
in0BlockWlands far belowkPerCore(or whennum_blocksexceeds a bound) and fall back to mcast.computeShardParamsalready returns
std::nulloptfor the no-fit case, so this is a guard in thesame place, not a new mechanism and not a fitted threshold. On the measured
set it removes every p150 regression while keeping all nine at-max wins.
in1CB depth. tt-metal triple-buffers it(
in1_CB_tiles *= 3whenB*num_blocks > 1); tt-mlir's model matchesexactly, so the 1,122,816 B is real. At ×2 it becomes 748,544 and
ibw=43fits within
cbBudgetoutright. This is a tt-metal pipeline-depth change andrisks throughput on cases that already work — measure before adopting.
kNumIn0Coresbecomes a lever, search it per-op — do not swap it foranother constant. See §7.1 below.
qwen_2_5_1_5bgap separately. Its +23.5% / +10.1% is not a DSwin: its decode graph previously had no matmul program config at all, so DS
was beating TTNN's default heuristic. Any change that turns DS off for it
re-exposes that gap. The durable fix is emitting a proper mcast config there.
7.1 Why
kNumIn0Cores = 8is not a grid number, and why 10/11 are wrongThe in0 activation is a single width-sharded L1 row — emitted as
<1x8>atcore_range<(0,0),(7,0)>— soncis bounded by the grid width: 8 onWormhole, 11 on Blackhole (confirmed by p150 output core_ranges
(0,0)-(10,3)+(0,4)-(3,4)= 48 cores). That makes "8 = one Wormhole row"a tempting reading, and suggests 10 or 11 for Blackhole.
The measurements say otherwise. The binding constraint is the eligibility gate
kTiles % nc == 0withkTiles = K/32, evaluated over 2750 DS matmul instancesand 20 distinct K values sampled across all three platforms:
ncObserved
kTiles: 24, 32, 40, 48, 64, 80, 96, 112, 128, 160, 216, 224, 256,280, 304, 344, 384, 448, 512, 720.
8 works because transformer K dims are essentially always multiples of 256, so
kTilesis essentially always a multiple of 8. Its equality with Wormhole'srow width is a coincidence of two unrelated facts (tile width 32 combined with
model dims, versus a physical grid dimension). 11 divides none of the observed
kTiles; 10 divides only the five that are multiples of 320 (K = 1280, 2560,5120, 8960, 23040). Setting
nc = 10or11on Blackhole would not retune DS —it would disable it for 82% / 100% of matmuls, "fixing" the p150 regression by
switching the feature off.
As an L1 lever
nccuts both ways, sincein1CB = in0_block_w × shardWTiles × weightTile × 3andin0_block_w ≤ kTiles/nc:ncshrinksibw_maxand so shrinksin1CB;ncgives the walk-down more divisors to land on.For the K=11008 case (
kTiles=344;q=43is prime atnc=8):nc=2is 4× better on the worst case at full coverage, butin0TensorBuf = perCoreM·q·2048grows 88 KB → 352 KB, consuming the budget itfrees, and 2 mcast senders will likely hurt the shapes that are currently fine.
nc=16halvesin1CBbut needs the in0 row to wrap across grid rows, and itdoes not rescue the bad cases: K=11008 becomes ineligible, and p150
falcon3_7bstill needs45 × 90 × 1088 × 3 ≈ 13 MBbecause its per-bank shardis 90 tiles wide.
Conclusion: no single global constant is correct. The good
ncdepends onthe factorization of
kTilesand onshardWTilesfor that specific matmul. Ifncis made a lever it should be searched per-op jointly within0_block_wtominimize
num_blockssubject to the L1 budget. The decline-on-collapse guard(recommendation 1) remains the cheaper first move.
Minor modelling discrepancies found in
computeShardParamsoutTensorBufPerCoreusesnumIn0Coreswhere the resident output shard isperCoreM × perCoreN(which already derives fromnumOutCores). For a1×8960 output on a 110-core grid that reserves 35 tiles against 3 resident
(~66 KB of phantom L1). But relaxing it frees only ~14 KB on the −28.1%
matmul, which needs ~52 KB, so it does not rescue that case — and given §4b's
L1 CB clash, loosening the budget is the wrong direction.
out_reshard_CB_size(
per_core_M × per_core_N_storagetiles), a small under-estimate in theopposite direction.
8. Status of the stashed patch
A candidate fix exists in
git stashon the tt-mlir submodule (detached HEAD atae602833), 11 tracked files +136/−9 plus one new lit test instash@{0}^3:archPrefersDRAMSharded)force-dram-sharded-matmulpipeline option to bypass itoutTensorBufPerCoredenominator changeIt was verified green (75 lit tests: 72 pass / 2 unsupported / 1 XFAIL; 305
OptimizerTests; 13 DS gtests) but should not land as written:in0_block_wcollapse, which is arch-correlated but not arch-determined.Pre-existing and unrelated:
OpModelStrategyTestsaborts inScoreCandidateL1ShardedResultandReshardCandidatesTest.WithTensorLayoutsMapDoesNotCrash(
deriveCanonicalL1CoreRangeSet:gridShape[1] == 1Ufailed, was4 == 1).Confirmed by stashing the diff, rebuilding, and reproducing on clean
ae602833.9. Caveats
control spread (±3% on n150/p150, ±1.6% on qb2) is the practical noise floor;
treat individual deltas under ~3% as directional only. Medians are the
trustworthy figures.
skip device perf: true.action-download-artifactcodeload timeout andTT_FATAL @ topology_mapper.cpp:546), so those have no p150 comparison.dispatched with tt-mlir
626a734b, which was force-pushed out of the branchand is the head of no ref.
docker-build / build-imagedied withfatal: reference is not a tree: 626a734b...(exit 128) →buildskipped →the CI image tag was never pushed → every perf job failed container init with
Docker pull failed/Value cannot be null. (Parameter 'ContainerId').correlation, consistent with prefill being untouched.
10. Reproducing
All reactions