Skip to content

openPangu: fused/specialized ops for prefill and decode throughput - #2159

Closed
joelfarthing wants to merge 4 commits into
ikawrakow:mainfrom
joelfarthing:filament/openpangu-mega-ops
Closed

openPangu: fused/specialized ops for prefill and decode throughput#2159
joelfarthing wants to merge 4 commits into
ikawrakow:mainfrom
joelfarthing:filament/openpangu-mega-ops

Conversation

@joelfarthing

@joelfarthing joelfarthing commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

[superseded by #2168 et seq.]

This is the second of two stacked PRs; it depends on #2158, the shared indexer_topk fix (q8_1 scratch sizing, a CPU support predicate, a CUDA graph-node identity fix), split out so the one change reaching DeepSeek2 and GLM-DSA is reviewable on its own diff. This one is the openPangu operator suite that builds on it. I'm aware it is large, and I don't necessarily expect a quick review or a clean merge. I wrote the operators to be usable beyond openPangu, with GLM-DSA and DeepSeek v4 in mind, so I hope some are useful even if openPangu adoption isn't.

Currently, openPangu-2.0-Flash runs its DSA attention, indexer, and MTP heads on a bespoke manual graph. It works, but leaves throughput and compute-buffer on the table at five sites: the latent-attention downstream reconstruction, the MTP post-norm RMSNorm+residual, the offset RoPE on the DSA indexer K/Q, the DSA cache-row writes, and (without -fidx) the top-k selection. This PR adds a fused or specialized op for each and routes openPangu through them, capability-gated: a layer adopts the fused path only when its scheduled backend can execute the candidate node, and each candidate is prechecked with a non-aborting validity predicate, so an unsupported backend or a layout the op cannot represent falls back to the byte-identical legacy chain instead of a scheduler-inserted CPU island or a constructor abort. The CPU backend reports no support for the latent-attention op, so a CPU-only or -ngl 0 run keeps the vectorized legacy attention.

Selection is not driven by per-op tunables: the default is fixed and capability-gated, and the only override is a compile-time -DGGML_OPENPANGU_LEGACY_OPS=1, which forces every legacy chain at once (all six ops revert together) and dominates an explicit -fidx. It's not read from the environment, so a compiled binary carries one fixed policy. The effective policy prints in the load log (openpangu_ops = fused (capability-gated) or legacy-forced, plus a dsa_idx_topk_policy line).

The operators, grouped by what they do:

  • Latent-attention downstream fusion. ggml_latent_attn_prefix_ext and ggml_latent_attn_indexed_ext replace the gathered/scored latent-attention reconstruction on the openPangu attention path (dense, SWA, and the gathered DSA route). This is the largest single piece and the biggest measured win. The op takes an F32 query and reads the raw latent K cache directly in F32, F16, or Q8_0, dequantizing internally; the optional mask is F32. When a layer's backend cannot run the produced node, the layer keeps the full legacy chain, including the get_rows subchunk loops.
  • Fused RMSNorm + add. ggml_fused_rms_norm_add folds the post-norm RMSNorm and its residual add into one op at the two openPangu MTP post-norm sites. The fused GGML_OP_FUSED_RMS_NORM_ADD node is produced only for the canonical case (contiguous F32 weight matching the normalized dimension); an F16 or otherwise non-canonical weight is decomposed by the constructor into rms_norm + mul + add, and a backend that cannot run the fused node likewise takes the decomposed path. This op is only in the MTP graph, so it is dormant in ordinary decode; see the validation caveats.
  • Offset RoPE. ggml_rope_ext_offset applies NEOX RoPE over a channel interval with a same-pass passthrough of the untouched channels, used at the two DSA indexer K/Q sites. Inputs are F32. The main-Q pack is deliberately not converted, because its no-rope branch is a projection rather than a split/reconcat. Both openPangu call sites pass a zero interval offset, which is byte-for-byte the existing NEOX kernel; the nonzero-offset generality is carried for the rope-first packing a future GLM/DeepSeek adopter needs, and the shared CUDA and CPU NEOX paths are verified equivalent at offset zero so no existing NEOX model changes.
  • Cache-row packing. ggml_pack_cache_rows packs two F32 sources into one destination cache row per token, with F32/F16/Q8_0 destinations and a block-aligned split for the quantized path, replacing the ggml_cpy-based DSA cache writes. It carries a small openPangu cache-write layer that validates the base and MTP write sets and retargets the write nodes for graph reuse.
  • Backend-gated automatic INDEXER_TOPK. openPangu uses the existing shared GGML_OP_INDEXER_TOPK automatically when the scheduled backend can execute it, and keeps the legacy chain when the indexer projection placement is split or unresolved. One honest exception to the capability gate: an explicit -fidx forces this op on regardless of backend support, preserving the existing DSA opt-in semantics, so -fidx on a backend that cannot run the op is the one way to get a scheduler-inserted CPU island here. Every other op in this PR is gate-only, with no force override. A -DGGML_OPENPANGU_LEGACY_OPS=1 build forces the legacy chain and takes precedence even over an explicit -fidx. The correctness work on the op's CUDA path is not in this PR: it is the prerequisite indexer_topk fix this one depends on (see Scope and blast radius).
  • Batched mix. ggml_batched_mix_ext fuses the mHC post-block combine, the per-token contraction R_new[h,s,t] = sum_j m[s,j,t] * R[h,j,t] over the Sinkhorn doubly-stochastic mix, into one strided token-batched matrix op (r [D,J,T] by mix [J,O,T] to [D,O,T]). It produces all S output streams in a single pass. The existing ggml_mul_multi_add fuses only the single-output reduce (sum_j r[j]*w[j]), so an S-output mix would take S passes through it, which is exactly the per-slot legacy loop this op replaces. F32, capability-gated like the rest, one openPangu call site.

Scope and blast radius. The new operators are constructed only from build_openpangu.cpp (and, for pack-cache, the openPangu cache-write helper). No other model builder calls any of them; I searched every builder to confirm it. The new enum entries, name/symbol tables, CPU compute functions, and CUDA dispatch/support cases in the shared files (ggml.h, ggml.c, ggml-cuda.cu) are dormant unless the op is actually built, so they are inert for every other architecture.

Two touches genuinely reach past openPangu:

  • INDEXER_TOPK is the one piece reaching other families, and its correctness rework (q8_1 scratch, CPU predicate, src_type graph identity) is the prerequisite indexer_topk: fix quantized q8_1 scratch sizing on CUDA #2158, carried on its own diff. Here openPangu just adopts the op under the capability gate; build_deepseek2.cpp and the op's numerical contract are untouched. This PR does extend indexer_topk: fix quantized q8_1 scratch sizing on CUDA #2158's src_type graph-identity snapshot to the new type-specialized ops (LATENT_ATTN, PACK_CACHE_ROWS, BATCHED_MIX), and the op-params comparison to LATENT_ATTN and BATCHED_MIX, so their captured kernels cannot replay stale; no existing op's graph reuse is loosened.
  • The CPY kernels take a gated write-indirection parameter for pack-cache-rows (cdst_indirect / graph_write_index). It is gated: use_write_indirection defaults false, and a null indirect pointer falls through to the original direct destination, so generic CPY is behavior-identical for every non-openPangu model — a signature change, not a behavior change. This PR also frees the shared dest_ptrs_d table in the ggml_cuda_graph destructor, which base leaks on every graph that uses indirection (the realloc path already frees it, so freeing at destruction is safe); the fix closes the leak for every CPY-indirection user.

No other architecture's graph, cache layout, or state handling changes. I traced the shared files to confirm the additions are registration and dispatch, not edits to existing op behavior.

For GLM-DSA and DeepSeek v4 use: these are generic ggml ops, not openPangu-hardcoded. ggml_batched_mix_ext, ggml_rope_ext_offset, and ggml_pack_cache_rows are F32 with no openPangu assumptions; ggml_fused_rms_norm_add takes F32/F16 weights; the latent-attention ops carry the MLA split (dv, dv_off) and the indexed route as runtime arguments, so any latent-KV-plus-indexer architecture supplies its own dimensions behind the same gate. The ggml core carries no openPangu constants; the openPangu specifics stay in build_openpangu.cpp.

I've watched #2147 (the DeepSeek v4 work) and checked this suite against it directly. It does not reimplement any of these operators: its DeepSeek v4 attention is assembled from ggml_flash_attn_ext, a ggml_set_rows top-k mask, and the shared ggml_indexer_topk, and none of ggml_latent_attn_*, ggml_rope_ext_offset, ggml_pack_cache_rows, ggml_batched_mix_ext, or ggml_fused_rms_norm_add appear in its diff. Where the two touch the same ground it is confirming rather than conflicting: DeepSeek v4 calls the shared GGML_OP_INDEXER_TOPK from its own builder, joining build_deepseek2.cpp and openPangu as a further in-tree caller of the op this stack fixes. The two diffs intersect in eight files, but the overlap is almost entirely additive registration and dispatch: op enums and names in ggml.h/ggml.c, the CUDA dispatch in ggml-cuda.cu, the legacy-ops CMake option, and config plumbing in llama-context.h/llama-cparams.h/llama.cpp. The one substantive shared edit is build_openpangu.cpp, where #2147 swaps a mul plus sum_rows_ext for the existing ggml_mul_multi_add at the mHC pre-combine, a few lines from this PR's post-combine work; it resolves on rebase. My ggml_latent_attn_indexed_ext computes in one op the same indexed latent attention that DeepSeek v4 currently assembles from three, so if that consolidation is useful there, the adoption pattern is the openPangu one, gated the same way.

Cross-architecture usefulness is a design-intent and shared-op claim, not validated. The exception is INDEXER_TOPK, which is already shared and now has a further caller in #2147.

Contracts. The inference-only constructors reject gradient-bearing inputs; the latent-attention op requires a float-aligned mask row stride (CUDA matched byte-exact to the CPU reference); quantized latent caches and the pack-cache quantized split must be block-aligned. The constructors assert these.

Validation. Re-established on the converged branch (head 2879e4466). On an RTX 4070, CUDA 13.3, GCC 13.3, openPangu-2.0-Flash-Q4_K_M with -ngl 999 -ot exps=CPU -c 32768 -ictk q8_0 -fidx -dsatk 64 greedy: the fused build and a -DGGML_OPENPANGU_LEGACY_OPS=1 legacy build both generate coherent output, and the load log confirms the effective policy on each: openpangu_ops = fused (capability-gated) with dsa_idx_topk_policy = explicit-force, versus legacy-forced (-DGGML_OPENPANGU_LEGACY_OPS=1) with dsa_idx_topk_policy = legacy-forced even with -fidx passed, so the compile-time switch dominates an explicit request.

Quality on the fused build, four fixtures generated and then run rather than eyeballed: bulgaria (1300 years, coherent, no repetition, no CJK bleed), sayap (correct alignment, no preamble), quicksort (sorts correctly over 200 random cases), double-linked-list with its pytest (14 of 14 pass). All four finished on stop, no reasoning-token leak, decode stable at 17.07 to 17.17 tok/s.

Lossless fused-versus-legacy: at the converged head, in the deployed regime (-c 34816 -ctk q8_0 -ictk q8_0 -fidx), the fused build and a compile-time -DGGML_OPENPANGU_LEGACY_OPS=1 build both generate coherent output; on the earlier pre-split build the fused ops' greedy streams were byte-identical to the legacy chains. The restoration of 32K retrieval and near-lossless q8_0 perplexity relative to current main comes from the prerequisite indexer PR #2158; this PR's job is to add the fused ops without regressing that.

The per-op focused CPU/CUDA correctness tests pass on the operators; a few assertions in the held-out suite pinned pre-split behavior and are realigned to this PR's contract. The tests are held out of this diff to keep it to the production change, and I can supply them on request.

openpangu-mega-ops-perf

Performance. Warm A/B/A/B, medians of two passes each, measured on this branch's head 2879e4466 against current main 9d07d8681 (both llama-sweep-bench binaries' sha256 recorded with the logs). openPangu-2.0-Flash-Q4_K_M on an RTX 4070, experts on CPU: -ngl 999 -ot exps=CPU -fa 0 -ctk q8_0 -ictk q8_0 -fidx -dsatk 64, sweep -c 34816 -b 2048 -ub 2048. This is the deployed long-context regime (q8_0 K-cache and q8_0 indexer cache).

Exact medians (llama-sweep-bench):

N_KV PP main PP this PR TG main TG this PR
0 257.6 284.2 15.26 17.34
2048 259.6 289.5 13.58 17.27
8192 249.1 281.6 13.38 16.91
16384 239.7 266.6 13.21 16.69
24576 231.1 256.9 13.07 16.39
32768 220.4 245.0 12.91 16.20

Prefill is a steady ~1.11× across the curve; decode holds while main sags (1.14× at 0 to 1.25× at 32K; 93.4% vs 84.6% retention), with no collapse at the ceiling. The throughput is the fused ops; the prerequisite indexer fix is a correctness change, expected to be throughput-neutral (not separately swept). The 32K CUDA compute buffer is 2.29× smaller (2873.27 to 1256.02 MiB, 14361 to 8473 graph nodes); KV (1009.71 MiB) and host (312.28 MiB) buffers are identical, so the reduction is the fused graph, not a cache change.

MTP is still not a win. ggml_fused_rms_norm_add lives only at the MTP post-norm sites, so it's not exercised in ordinary decode, and MTP self-speculation loses in this CPU-expert-offload regime (heads=1 runs 10.7 to 12.1 tok/s versus 17.1 non-MTP, and it degrades further with draft depth and with heads=3), because the batched verify pays the CPU expert cost every step. Measured in MTP with the op on versus off, its throughput contribution is within noise and the greedy token streams match. It earns its place on correctness and on removing a graph node, not on a decode-speed number in this regime. I suspect that MTP needs the whole model on GPU for a win.

  • I have read the contributing guidelines
  • Self-reported review complexity:
    • Low
    • Medium
    • High

The quantized-K path under-sized its q8_1 scratch buffer: it allocated q->ne[1]*max_rows blocks using the unpadded head dim, but quantize_mmq_q8_1_cuda writes q_padded/QK8_1 blocks per row and must process all q->ne[1]*nrows rows. Size the buffer by (q_padded/QK8_1) blocks x (q->ne[1]*max_rows) rows and pass the full q->ne[1]*nrows row count so the scratch cannot be overrun and every query row is quantized.

CUDA graph identity: INDEXER_TOPK dispatches a source-type-specialized kernel (dense F16 vs quantized cache, F32 vs F16 mask). Source addresses alone do not identify the captured kernel, so snapshot each source type in ggml_graph_node_properties and force re-capture when an INDEXER_TOPK source type changes, preventing a reused graph from replaying the wrong kernel variant when sources are reallocated at the same address.

CPU backend: report INDEXER_TOPK support via iqk_indexer_topk_supported (guarded by GGML_USE_IQK_MULMAT) so the scheduler places the node on a backend that can run it. Harden the scheduler's pass-5 node-assignment check from assert to GGML_ASSERT so a node no backend supports fails as a defined abort under NDEBUG instead of indexing sched->backends[-1].
…ated)

Adds the OpenPangu-2.0-Flash fused/specialized op path: latent attention over a packed K/V cache (prefix + indexed variants), fused RMSNorm+add, offset RoPE, batched mix, and pack-cache-rows, wired into the OpenPangu graph builder alongside the shared INDEXER_TOPK op. Each op ships a CUDA kernel plus the exact legacy ggml chain it replaces.

Selection is capability-gated: an op is used only when the scheduled backend reports support for the exact node (ggml_backend_supports_op, anchored on per-layer weights), and falls back to the legacy chain otherwise, so a partially-supporting backend never strands a node. GGML_OPENPANGU_LEGACY_OPS is a single compile-time bring-up switch (build with -DGGML_OPENPANGU_LEGACY_OPS=1) that forces every legacy chain at once for A/B baselines; it dominates an explicit -fidx request, and the effective policy is reported in the model load log (openpangu_ops).

The fallback also covers operand contract, not just backend capability: each fused candidate is prechecked with a non-aborting validity predicate (ggml_latent_attn_ext_valid, ggml_rope_ext_offset_valid, ggml_pack_cache_rows_valid, ggml_batched_mix_ext_valid) before construction, so a layout the op cannot represent takes the legacy chain instead of aborting in the constructor. The CPU backend reports no support for LATENT_ATTN, so its scalar reference implementation stays a correctness/testing path and a CPU-only or -ngl 0 run keeps the vectorized legacy attention rather than silently adopting the reference op.

Builds on the INDEXER_TOPK quantized-CUDA scratch-sizing fix in the parent commit; the OpenPangu graph selects that op on backends that support it.
Comment thread ggml/src/ggml-cuda/cpy.cu Outdated
const int ne00, const int ne01, const int ne02, const int nb00, const int nb01, const int nb02,
const int nb03, const int ne10, const int ne11, const int ne12, const int nb10, const int nb11,
const int nb12, const int nb13, char ** cdst_indirect, int graph_cpynode_index) {
const int nb12, const int nb13, char ** cdst_indirect, int graph_write_index) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if the variable name does not correspond to your taste, I see zero reason to change 119 LOC for no real reason.

Comment thread ggml/src/ggml-cuda/graph.cuh Outdated
bool use_cpy_indirection = false;
std::vector<char *> cpy_dest_ptrs;
char ** dest_ptrs_d;
// CPY and destination-rooted mutation ops share one graph-order pointer table.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please no renaming. What is it that we gained by changing the name other than to increase the review burden?

Revert the graph_cpynode_index / use_cpy_indirection / cpy_dest_ptrs field
names and the ggml_cuda_cpy_dest_ptrs_copy helper to their original spellings.
PACK_CACHE_ROWS reuses this graph-order pointer table, so the machinery is no
longer CPY-only, but renaming it is not required for the sharing and only
inflates the diff. Keep the original names and note the shared use in a comment.

No functional change: the destructor cudaFree(dest_ptrs_d) leak fix, the
dest_ptrs_d nullptr init, the ggml_graph_node_properties::src_type field, the
PACK_CACHE_ROWS branch, and the op-conditional destination push_back are all
retained.
@joelfarthing

Copy link
Copy Markdown
Contributor Author

Thanks for review! Reverted to the original names. PACK_CACHE_ROWS reuses the same table, so cpy is slightly less literal now, but the rename isn't needed for the sharing. I kept a leak fix in the destructor and the shared-table comment.

Comment thread ggml/src/ggml-cuda/latent_attn.cu Outdated
local_max = fmaxf(local_max, v);
}
shbuf[tid] = local_max;
__syncthreads();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have things such as warp_reduce_sum, warp_reduce_max, etc.

Comment thread ggml/src/ggml-cuda.cu Outdated
return op->src[0]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32 &&
sink_s >= 1 && sink_s <= 8 && op->src[0]->ne[0] == (int64_t) sink_s*sink_s;
}
case GGML_OP_LATENT_ATTN: {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are writing such extensive checks then please add a function that does that. This applies to all the long checks that you have added.

#include "../llama-build-context.h"
#include "../llama-model.h"
#include "../llama-context.h"
#include "openpangu-op-policy.h"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was disappointed to find out that after adding multiple new ops and thousands of lines of implementations, the build graph function actually became longer rather than shorter, as one would expect given the many fused ops.

@ikawrakow

Copy link
Copy Markdown
Owner

Tested the Q4_K_M model on 2x3090+Ryzen-3995WX using --fit --fit-margin 6144 (results in 20 MoE layers being left in RAM). Very decent PP speedup increasing with context length, more modest TG performance gain.

This PR

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 2.471 828.74 1.684 38.01
2048 64 2048 3.030 675.96 1.734 36.92
2048 64 4096 3.209 638.26 1.735 36.89
2048 64 6144 3.226 634.77 1.758 36.40
2048 64 8192 3.252 629.68 1.772 36.13
2048 64 10240 3.336 613.95 1.762 36.33
2048 64 12288 3.345 612.24 1.767 36.21
2048 64 14336 3.375 606.90 1.780 35.95
2048 64 16384 3.469 590.37 1.767 36.22
2048 64 18432 3.527 580.60 1.884 33.96
2048 64 20480 3.577 572.58 1.883 33.99
2048 64 22528 3.644 561.98 1.901 33.67
2048 64 24576 3.712 551.70 1.890 33.86
2048 64 26624 3.711 551.95 1.843 34.73
2048 64 28672 3.845 532.66 1.814 35.27
2048 64 30720 3.862 530.29 1.815 35.26
2048 64 32768 3.911 523.62 1.838 34.82
2048 64 34816 4.004 511.46 1.826 35.05
2048 64 36864 4.044 506.42 1.825 35.06
2048 64 38912 4.084 501.43 1.937 33.04
2048 64 40960 4.185 489.38 1.877 34.11
2048 64 43008 4.178 490.13 1.926 33.23
2048 64 45056 4.197 487.92 1.963 32.60
2048 64 47104 4.373 468.33 1.965 32.57
2048 64 49152 4.341 471.80 2.012 31.81
2048 64 51200 4.404 465.08 1.911 33.48
2048 64 53248 4.446 460.67 1.922 33.29
2048 64 55296 4.483 456.83 1.866 34.29
2048 64 57344 4.585 446.65 1.937 33.04
2048 64 59392 4.598 445.44 1.884 33.97
2048 64 61440 4.645 440.91 1.874 34.15
2048 64 63488 4.716 434.24 2.002 31.96

Main branch

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 2.730 750.30 1.778 36.00
2048 64 2048 3.869 529.28 2.187 29.26
2048 64 4096 5.629 363.84 2.317 27.62
2048 64 6144 6.069 337.45 2.224 28.78
2048 64 8192 6.236 328.44 2.220 28.82
2048 64 10240 6.281 326.07 2.232 28.67
2048 64 12288 6.447 317.69 2.263 28.29
2048 64 14336 6.584 311.04 2.250 28.44
2048 64 16384 6.889 297.30 2.311 27.70
2048 64 18432 6.923 295.84 2.382 26.87
2048 64 20480 7.125 287.44 2.271 28.19
2048 64 22528 7.323 279.65 2.304 27.78
2048 64 24576 7.741 264.57 2.311 27.70
2048 64 26624 7.851 260.85 2.455 26.06
2048 64 28672 7.963 257.19 2.314 27.65
2048 64 30720 8.154 251.16 2.328 27.50
2048 64 32768 8.320 246.16 2.329 27.49
2048 64 34816 8.529 240.12 2.355 27.18
2048 64 36864 8.844 231.57 2.407 26.59
2048 64 38912 8.909 229.89 2.360 27.12
2048 64 40960 9.150 223.83 2.356 27.16
2048 64 43008 9.294 220.35 2.380 26.89
2048 64 45056 9.470 216.25 2.382 26.87
2048 64 47104 9.668 211.84 2.393 26.74
2048 64 49152 9.836 208.21 2.392 26.76
2048 64 51200 10.194 200.90 2.395 26.72
2048 64 53248 10.518 194.72 2.478 25.82
2048 64 55296 10.451 195.97 2.470 25.91
2048 64 57344 10.572 193.71 2.447 26.16
2048 64 59392 10.768 190.19 2.429 26.34
2048 64 61440 10.946 187.10 2.449 26.13
2048 64 63488 11.104 184.45 2.448 26.15

@ikawrakow

Copy link
Copy Markdown
Owner

As a general comment: I feel too much new stuff is being added. I need to think some more.

Comment thread src/graphs/build_openpangu.cpp Outdated
ggml_tensor * q_idx_offset = nullptr;
if (openpangu_rope_offset_enabled() &&
ggml_rope_ext_offset_valid(q_idx, inp_pos, nullptr, n_rot, 0, rope_type)) {
q_idx_offset = ggml_rope_ext_offset(ctx0, q_idx, inp_pos, nullptr, n_rot, 0, rope_type,

@ikawrakow ikawrakow Jul 21, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is funny. We add a whole new op so we can skip the so called "legacy route". We then use a rope offset of zero. Which is 100% equivalent to just using the exiting ggml_rope_ext directly.

If in doubt, just change ggml_rope_ext_offset to ggml_rope_ext, remove the extra 0 argument between n_rot and rope_type, and see what happens. When you find that it works like that, remove all the checking if we can use that, remove the new GGML_OP_ROPE_OFFSET op and associated ggml_rope_ext_offset API, etc.

The reason you find this pattern of making two views of a tensor, doing RoPE one one of the views, and then concatenating them back together is I guess laziness to check what gets copied unmodified for the different RoPE types. But in this particular case, it just worksTM.

On a second thought: don't remove the op. I see that the DS4 implementation is full of the "legacy" pattern.

@joelfarthing joelfarthing Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought: don't remove the op. I see that the DS4 implementation is full of the "legacy" pattern.

Right, the zero-offset validates here but was designed for nonzero-offset to replace DS4's repeated split/RoPE/concat chains. I tried to make all of these ops portable in that way.

As a general comment: I feel too much new stuff is being added. I need to think some more.

I totally understand that, and would also understand keeping this as an unmerged reference branch if that's better.

Also: I made an IQ4_NL of openPangu that reduces overhead a bit and seems to outperform the Q4_K_M on my usual agent tests. It's at https://huggingface.co/ji-farthing/openPangu-2.0-Flash-ik-llama-GGUF in case helpful.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I did a quick experiment with DS4 where avoiding the concatenation between RoPE'd and not RoPE'd parts could matter. I see zero difference on the CPU. On the GPU, I see ~1% performance gain for PP and no performance gain for TG. So, basically, I don't think it is worth the extra code and extra op.

But if you think it is worth it, please extract the RoPE change into a separate PR. Please no new op, and please don't add I don't know how many checks. Strictly speaking the addition is a boolean flag, and not an offset. With an offset there is the possibility to have a [no-RoPE, RoPE, noRoPE] configuration, which I don't think is useful in practice. We only care about [RoPE, noRoPE] (handled by the current implementation) or [no RoPE, RoPE] (triggered by an added boolean flag that can go into op_params[15]. So, then, one can simply set the flag in the graph building code, but if we wanted to be more professional, there would be a function in the API such as ggml_rope_set_flipped that sets op_params[15] to 1(after checking that n_dims is actually less than src[0]->ne[0].

@joelfarthing
joelfarthing marked this pull request as draft July 22, 2026 12:47
@ikawrakow

Copy link
Copy Markdown
Owner

OK, below is a sweep-bench where I have turned off the GGML_OP_LATENT_ATTN (by just making the conditions that use it in build_openpangu() to be always false). Comparing to the above, we see that for TG almost all of the performance improvement comes from that. PP is still noticeably better than the main branch for long context, but most of the performance improvement still comes from using GGML_OP_LATENT_ATTN.

Given this, lets do the following: lets split out the GGML_OP_LATENT_ATTN related changes into a separate PR to be reviewed and merged first. Please lets not pretend that we are writing airplane control software, and use that to reduce the noise from endless applicability checks.

Looking at the arguments of ggml_flash_attn_ext and ggml_latent_attn_prefix_ext / ggml_latent_attn_indexed_ext, they are basically the same except for the addition of prefix_k and prefix_v. So, theoretically, one could also just refrain from adding a new op, add a new function (e.g., ggml_flash_attn_set_prefix(ctx, prefix_k, prefix_v, mode)), where all the checking if one can actually do that are done, and then invoke the implementation from FA in a similar way as I have done with the DSA attention.

Once that happens, and hopefully I have also merged the DS4 branch by then, we can go one by one with the remaining ops, where it should be demonstrated that they are useful for openPangu and DS4 (and possibly other arches if applicable).

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 2.591 790.46 1.823 35.10
2048 64 2048 3.044 672.88 2.188 29.25
2048 64 4096 4.704 435.36 2.294 27.90
2048 64 6144 4.930 415.41 2.228 28.73
2048 64 8192 4.971 412.02 2.227 28.74
2048 64 10240 5.049 405.63 2.255 28.39
2048 64 12288 5.046 405.90 2.307 27.75
2048 64 14336 5.082 403.03 2.232 28.67
2048 64 16384 5.183 395.12 2.238 28.60
2048 64 18432 5.241 390.78 2.246 28.50
2048 64 20480 5.279 387.97 2.409 26.57
2048 64 22528 5.335 383.87 2.284 28.01
2048 64 24576 5.428 377.29 2.327 27.51
2048 64 26624 5.433 376.99 2.421 26.43
2048 64 28672 5.551 368.96 2.371 26.99
2048 64 30720 5.576 367.31 2.292 27.92
2048 64 32768 5.629 363.84 2.300 27.83
2048 64 34816 5.718 358.14 2.417 26.48
2048 64 36864 5.758 355.69 2.310 27.70
2048 64 38912 5.793 353.55 2.374 26.96
2048 64 40960 5.891 347.68 2.358 27.14
2048 64 43008 5.892 347.59 2.385 26.83
2048 64 45056 5.904 346.89 2.460 26.02
2048 64 47104 6.071 337.33 2.331 27.46
2048 64 49152 6.052 338.39 2.316 27.63
2048 64 51200 6.138 333.64 2.316 27.64
2048 64 53248 6.175 331.65 2.370 27.01
2048 64 55296 6.215 329.50 2.358 27.14
2048 64 57344 6.298 325.20 2.423 26.41
2048 64 59392 6.330 323.56 2.337 27.38
2048 64 61440 6.382 320.89 2.338 27.38
2048 64 63488 6.418 319.11 2.351 27.22

@ikawrakow

Copy link
Copy Markdown
Owner

Oh, one more thing. As it stands, the PR does basically nothing for CPU-only inference.

Here is what I get on my Ryzen-3995WX CPU with the PR (with all changes included):

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 16.295 125.68 3.437 18.62
2048 64 2048 21.537 95.09 4.612 13.88
2048 64 4096 34.243 59.81 5.177 12.36
2048 64 6144 35.307 58.01 4.884 13.10
2048 64 8192 35.725 57.33 5.009 12.78

And this is on the main branch

PP TG N_KV T_PP s S_PP t/s T_TG s S_TG t/s
2048 64 0 17.672 115.89 3.495 18.31
2048 64 2048 22.380 91.51 4.632 13.82
2048 64 4096 34.965 58.57 5.187 12.34
2048 64 6144 35.999 56.89 4.894 13.08
2048 64 8192 36.254 56.49 5.026 12.73

@joelfarthing

joelfarthing commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

This PR superseded by #2168 et seq.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants