Repository navigation
Conversation
The selection mask and the indexer scores of a QSA layer are [n_kv, n_tokens] and [n_pool, n_tokens]. Score, select and attend 2048 query tokens at a time, so the chunks of a layer reuse one chunk worth of memory. The views of the token inputs are shared by the layers, so each slice is uploaded once. LLAMA_QSA_CHUNK sets the chunk size for tests, 0 disables the split.
A [n_kv, 1, 1, 1] mask applies to every token and gives a [n_kv, n_tokens] result, so a selection that is causal on its own does not need a dense [n_kv, n_tokens] mask as its source.
Like llm_graph_input_attn_kv, do not fill a mask that no op reads.
A token sees only the pools that end at or before it and the tail up to itself, all of its own sequence, so every kept cell is already visible in the kq_mask and the selection is causal on its own. Keep a zero row instead of the kq_mask cells, then no op reads the dense [n_kv, n_tokens] mask, and the graph does not allocate or upload it.
Master has the final PR 30640, which renames ggml_kpool_mask to ggml_top_k_mask and broadcasts the mask natively; take its version of the op and call it with the zero row from qwen4exp.
|
@dagbdagb Replying here. Asked one of the agents to benchmark and find the best setting. |
am17an
left a comment
There was a problem hiding this comment.
Is the main problem here at long context? i.e. uploading the kq mask?
|
Not entirely sure if that question was directed at me or not. If it is: The help-text for |
|
@dagbdagb it was not for you. Re the guidance, yes I will make some changes there soon |
|
These are ideas pulled straight from Strata, with the goal to reduce memory footprint of work buffers:
Shrinking And there's a bug with CUDA top_k tie ordering - haven't looked into it.
1750 t/s pp is the major win here |
|
@am17an Feel free to slice and dice this thing into pieces as much as you want or need. |
| static int64_t qwen4exp_qsa_n_chunk() { | ||
| static const char * env = getenv("LLAMA_QSA_CHUNK"); | ||
| static const int64_t n_chunk = env ? std::max<int64_t>(0, atoll(env)) : 2048; | ||
| return n_chunk; |
There was a problem hiding this comment.
2048 was a sweet spot on 5090
at 1024 the launch tax starts to show up
|
Okay so this is because the kq_mask being the largest user of the compute buffer at these ub sizes. But do we need to allocate the kq_mask for this model since we're using sparse attention? cc @ServeurpersoCom if you have a good idea. EDIT: maybe I'm not correct and need to measure again. |
|
all 12 attention layers are QSA so we should not need kq_mask |
ggml_top_k_mask with a single keep row returns [n_kv, n_tokens], while build_attn_qsa expects the kq_mask layout [n_kv, n_tokens/n_stream, 1, n_stream]. With several streams the shape assert failed at graph reserve.
You are right, it is not needed for qwen4exp. The QSA selection is already causal and sequence local by construction: pool_mask only keeps pools of the token's own sequence that end at or before its rank, the tail holds its own cells up to itself, and dead picks or n_kv sentinels are skipped by top_k_mask. So kq_mask just multiplies the kept cells by zero, and in the real GGUF all 12 attention layers are QSA (the other 36 are GDN), so nothing else reads it. sanmai's numbers are correct, I get exactly the same on the real UD-Q4_K_XL: feeding top_k_mask a broadcast row of zeros instead of kq_mask takes the pinned host buffer from 5526 to 1430 MiB and the compute buffer down by 4 GiB at -c 262144 -ub 8192, which is the mask itself (262144 x 8192 x 2 bytes in F16). The logits at -c 16384 are byte identical to master. If we want to go all the way, what remains is the dense [n_kv, n_tokens] selection mask FA consumes, which the CUDA sparse FA path already turns back into indices for the shapes it covers. The query chunking in this PR bounds it, and a possible follow-up would be letting flash_attn_ext take the selected cell indices directly, which would remove it for qwen4exp, glm5-next and the DSA models. |
|
Just QSA without kq_mask for a pair of backends:
I extracted them because they pack the most punch (4 GB saved on GPU at ub 8192), but if that's a preference to keep this PR as a whole, I have nothing against it. |
|
I'm wondering how m-rope interacts with this whole thing. I don't have a good understanding of it at the moment in relation on how it interacts with the kq mask |
|
Tests are passing on my end. |
M-RoPE only matters for image tokens, which share pos[0]. kq_mask orders them by (y, x) through is_2d_gt, while the k-pool layout orders a sequence by (pos, cell) in kpool_rank, so ties go by cell index. An image is normally written in token order into increasing cells, so both orders agree and the selection is the same with or without kq_mask; a single image test on the real model gives identical output. They can only disagree when find_slot wraps around the ring in the middle of an image, so a later patch lands in a lower cell. That mismatch is already in the indexer on master: kq_mask only hides the half where the selection takes a later patch, it cannot bring back an earlier one the indexer dropped. The clean fix is to sort the k-pool layout by (pos, y, x), the ext positions are already read there for new_pool_pos, and then kq_mask is not needed in that case either. |
|
Okay then this should we merge this? |
I would start with #30965, which isolates the kq_mask part. I tested that exact diff on the real UD-Q4_K_XL: logits byte identical to master at -c 16384, -4 GiB on both the compute and pinned host buffers at -c 262144 -ub 8192, and the new test-backend-ops cases pass. The chunking here bounds the selection mask but keeps it dense, so I would hold it until we know whether the index based FA mentioned above is the way to go. |
|
it looks it may help with large ubatch on dsv4.1/glm5.3 as well |
Upstream's version is kept wherever it does what the branch did; the branch's stays where upstream has none: - MoE cache: the constructor takes upstream's no_alloc flag beside the CPU share, buffers come from llama_memory_bufs::alloc_buf; phases and host mode as before. - build_moe_ffn: the branch's experts lambda (cache slots, host mode, shared batches) with upstream's DeepSeek-4.1 and Laguna changes. - qwen4exp: the selection is built over the cache cells as upstream now does (ggml-org#30965, ggml-org#31243), the branch keeps its query chunks (ggml-org#30815's earlier version); upstream's hc_mix outputs and merged tensors. - CUDA MMQ: the tile map of active MoE tiles also launches upstream's fused gate/up/GLU kernel (ggml-org#31188, ggml-org#29948); the fused paths zero the rows of negative ids as ggml_cuda_mul_mat_id does. - CPU MUL_MAT_ID: upstream's -1 skip (ggml-org#26631) and the branch's two-pass grouping agree, negative ids are skipped. - tests: the QSA chunk tests move with upstream's archs tests; a MUL_MAT_ID result marked by ggml_mul_mat_id_set_negative_ids runs on CUDA again, only unmarked ones are excluded ([TAG_MUL_MAT_ID_SKIP]). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P3vd8iuiuxiev7wMPyJmSf
Overview
This is just a prototype. No review is needed.
Additional information
kq_maskBefore 72e03e1:
Requirements