OpenVINO #37435 — Why Dropping an ov::Output Port Could Turn TopK Indices into Values
OpenVINO GGUF Frontend, Part 2 of 3 — Multi-output tensor identity, dynamic k, and attention shapes that remain valid under both SDPA and PagedAttention
Part 1 examined how OpenVINO’s GGUF frontend separated model semantics from runtime-state ownership.
The frontend first produces a stateless graph with explicit KV-cache inputs and outputs.
A consumer that wants OpenVINO-managed state registers GGUFMakeStateful, which rewrites the cache path into:
ReadValue
↓
Gather by beam_idx
↓
Concat new cache rows
↓
AssignPR #37435 also repaired a different class of frontend failure.
These problems were not primarily about which operation should be used.
They were about preserving the exact identity and layout of the tensor produced by that operation.
A frontend does not translate only:
Add
MatMul
TopK
Reshape
TransposeIt must also preserve:
Which output port of the operation is being consumed?
Which axis represents tokens?
Which axis represents heads?
Which dimensions must remain statically known?
Which graph pattern must remain visible to a backend matcher?Those details can change the compiled result even when the high-level mathematical expression appears unchanged.
The clearest example was TopK.
OpenVINO’s TopK produces two outputs:
output(0)
→ selected values
output(1)
→ selected indicesGGML’s TOP_K and ARGSORT translators need the second one.
But an ov::Output<ov::Node> identifies more than the producing node.
It identifies:
producer node
+
output-port indexIf a translator converts that output into only:
std::shared_ptr<ov::Node>through:
get_node_shared_ptr()the port identity is no longer carried by the value.
A later operation that consumes “the node” can implicitly resolve it as output 0.
For TopK, that means the graph can silently move from:
indicesto:
valuesor fail when a downstream translator expects an integer index tensor but receives the values port instead.
PR #37435 therefore reinforced a broader frontend rule:
A tensor produced by a multi-output operation is not identified by its node alone.
Its output port is part of the tensor’s identity.
The same patch extended that principle into shape and attention translation.
A dimension was not treated as merely a number.
Its axis position, static knowledge, runtime placement, and relationship to backend graph matchers all had to be preserved.
A node and one of its outputs are different objects
For a single-output operation, the distinction can appear unimportant.
Consider:
Add
→ one outputWhether code passes:
add_nodeor:
add_node->output(0)the intended tensor is obvious.
A multi-output operation changes the contract.
For TopK:
TopK node
├── output 0: values
└── output 1: indicesThe node represents the complete operation.
The output represents one tensor edge leaving it.
In OpenVINO, that tensor edge is represented by:
ov::Output<ov::Node>It contains the producing node and the selected output index.
Conceptually:
TopK output 0
=
(TopK node, port 0)
TopK output 1
=
(TopK node, port 1)Those are not interchangeable tensors.
get_node_shared_ptr() retains the producer but not the selected edge
Suppose a translator correctly has:
topk->output(1)This identifies the index tensor.
Now suppose it calls:
topk->output(1).get_node_shared_ptr()The result identifies:
the TopK nodeIt no longer independently carries:
port 1If that node pointer is later passed into an API that accepts a node and implicitly selects its first output, the effective tensor becomes:
TopK output 0The operation is still TopK.
The tensor is different.
Before conversion
TopK node
+
port 1
→ indicesAfter reducing it to a node pointer
TopK node
+
implicit port 0
→ valuesThis is why a source graph can retain the correct producer while losing the correct data edge.
The PR description identifies this exact risk: taking .get_node_shared_ptr() can resolve a multi-output value to output 0, with TopK given as the concrete failure case.
TopK makes the mistake visible through type as well as meaning
The two outputs differ semantically.
They may also differ in element type.
A typical configuration is:
input
→ f32TopK output 0
→ f32 selected valuesTopK output 1
→ i32 or i64 selected indicesA downstream routing operation may require:
expert IDs
→ integer tensorIf it receives the values output instead:
floating-point scores
→ supplied where indices were expectedthe graph may fail conversion or validation.
In a more permissive path, an accidental conversion might allow execution while preserving the wrong semantics.
The danger is therefore not limited to a clean type error.
Correct node type
+
wrong output edge
→ incorrect tensor identityShape queries must also preserve the output port
The same boundary appears when a translator asks for a tensor’s shape.
OpenVINO’s GGUF helper has an overload shaped like:
get_dimensions(
const ov::Output<ov::Node>& output,
const std::vector<int>& dims
)It constructs ShapeOf directly from the selected ov::Output.
TopK output 1
↓
ShapeOf(output 1)
↓
Gather requested dimensionsThe helper does not first convert the output into a producer node.
The shape query therefore remains attached to the intended port.
The difference is:
Correct
ShapeOf(TopK output 1)
→ shape of indicesversus:
Incorrect
ShapeOf(TopK node)
→ implicit output 0
→ shape of valuesThe shapes may often be equal for TopK.
That does not make the port loss harmless.
The graph edge and its element type are still different, and another multi-output operation may have ports with different shapes.
The regression test checks the actual input-port index
PR #37435 added a structural test named:
GetDimensionsKeepsOutputPortThe test constructs a TopK, explicitly passes:
topk->output(1)into get_dimensions(), then inspects the ShapeOf input.
The assertion is not merely:
The resulting shape looks correct.It checks:
shape_of->input_value(0).get_index()
==
1In other words:
Did ShapeOf remain connected to TopK port 1?The test would fail if the helper measured output 0, even in a shape where both outputs happened to have identical dimensions.
This is a stronger regression contract than comparing shape values alone.
When two ports happen to have the same shape, a value-only test can pass while the graph still points to the wrong tensor.
Numerical equality cannot always detect identity loss
Suppose the two TopK outputs both have shape:
[2, 2]A test that asks only:
Did ShapeOf return [2, 2]?cannot distinguish them.
Values port shape
→ [2, 2]
Indices port shape
→ [2, 2]Both pass.
The port-index assertion checks the graph relationship directly.
Expected producer edge
→ TopK output 1not merely:
Expected dimension values
→ [2, 2]This is an important testing pattern for graph frontends.
Some correctness properties are topological.
They cannot be proven from the final numerical tensor alone.
ARGSORT and TOP_K now share one index-construction helper
Both GGML operations can be implemented through OpenVINO TopK.
TOP_K
Select the k largest elements
→ return their indicesARGSORT
Select the complete axis
→ order ascending or descending
→ return all indicesBefore the patch, the two translators built their own TopK nodes separately.
PR #37435 introduced:
make_topk_indices(...)The helper accepts:
input
k
axis
MIN or MAX mode
index element type
stable-order flagIt constructs the OpenVINO TopK and returns explicitly:
topk->output(1)The helper’s return type remains:
ov::Output<ov::Node>That keeps the index port attached to the tensor as it moves between translator utilities.
The helper centralizes a semantic choice, not just repeated code
The important shared rule is:
GGML TOP_K and ARGSORT want indices
→ OpenVINO TopK output 1If the translators construct TopK independently, they can drift.
One might use:
output(1)while another accidentally returns:
output(0)One might use the decoder-specified index type.
Another might hard-code i32.
One might derive k dynamically.
Another might require a static dimension.
The shared helper fixes one part of that contract in one location:
index output
→ always port 1The ARGSORT and TOP_K translators still determine their own k, axis, and mode, but they no longer independently decide which port represents the operation’s result.
ARGSORT uses the complete last dimension as k
GGML ARGSORT returns an ordering for every element along its sorting axis.
The translator therefore calculates:
k
=
size of the input’s last axisIt does not require that dimension to be a compile-time C++ integer.
Instead:
input
↓
ShapeOf
↓
Gather last-axis dimension
↓
Squeeze to scalar k
↓
TopKThe sort direction selects the TopK mode.
Ascending
→ MIN
Descending
→ MAXThe indices port becomes the translated output.
The current implementation derives the last axis from the static rank when possible and uses the frontend’s rank-4 convention as the fallback when rank information is not fully static.
The old TOP_K translator required a static output extent
The original TOP_K path used logic equivalent to:
const int64_t k =
output_shape[last_axis].get_length();get_length() requires the dimension to be static.
If the output extent was dynamic:
output last dimension
→ dynamicthe translator could throw during conversion before an executable graph was produced.
The problem was not that OpenVINO TopK requires a compile-time constant.
Its k input is itself a graph input.
The limitation came from the frontend materializing k as a C++ integer too early.
The new translator keeps a static fast path and a runtime fallback
The fixed translator first checks whether the relevant output dimension is static.
Static extent
Known output last-axis size
↓
create scalar Constant(k)Dynamic extent
Corresponding runtime input dimension
↓
ShapeOf / Gather
↓
Squeeze
↓
runtime scalar kThe translator can therefore construct the graph without calling get_length() on a dynamic dimension.
This is another form of preserving information.
Unknown at conversion timedoes not mean:
unknown at execution timeThe shape value can remain represented inside the graph rather than being demanded by the C++ translator.
Static and dynamic shape values have different owners
A static k is owned by the frontend at conversion time.
Frontend reads dimension
→ embeds ConstantA dynamic k is owned by runtime shape evaluation.
Frontend builds shape-expression graph
→ runtime supplies dimensionThe arithmetic operation is still TopK.
The source of the scalar is different.
A correct frontend must know which facts it may safely collapse into constants and which must remain graph values.
The shared helper does not force ARGSORT and TOP_K to have the same k
The two translators share:
TopK construction
index-port selection
index-type handlingThey do not share the exact meaning of k.
ARGSORT
k
=
full input dimensionTOP_K
k
=
requested output extentThe common helper accepts k as an ov::Output.
It does not decide where that scalar came from.
Shared mechanism
→ TopK indices
Operation-specific policy
→ how k is derivedThis is the same narrow-abstraction principle seen throughout the patch.
Share the contract that is truly common.
Do not merge distinct operation semantics merely because the final OpenVINO primitive is the same.
The TOP_K result order is not fully contractual in ggml
The added TOP_K unit test does not compare the returned indices position by position.
The test comments explain that ggml’s kernel deliberately swaps the first two result slots to emphasize that their order is not important.
The contractual property is the selected set.
For example:
Row:
1, 9, 3, 7, 5
k:
3
Expected index set:
{1, 3, 4}The OpenVINO output may list those indices in a deterministic value order.
The test sorts the returned indices before comparing the set.
This avoids converting an implementation detail of ggml’s output ordering into a stronger frontend contract than the source operation actually promises.
A frontend test should not demand semantics the source does not guarantee
An overly strict test might require:
[1, 3, 4]in exactly that order.
If ggml guarantees only:
the selected indices are {1, 3, 4}then an order-sensitive assertion would test the wrong contract.
The opposite error is also possible.
A test that checks only:
output type is i32would not prove that the correct experts were selected.
The regression chooses the supported middle ground:
correct index tensor
correct number of indices
correct selected set
no unsupported ordering guaranteeThe patch does not add a dedicated dynamic-k execution fixture
The source clearly adds a dynamic graph path for k.
The visible TOP_K runtime test in the merged patch uses a static output extent:
input last dimension:
5
output last dimension:
3The structural port test also uses a constant k.
I did not find a dedicated newly added runtime test in PR #37435 whose TOP_K output extent itself remains dynamic through conversion.
Therefore, the evidence should be separated.
Dynamic-k source path
→ directly confirmedDedicated dynamic-k runtime regression in this PR
→ not confirmedThe full frontend suite reported 137 passing tests, but that total alone does not identify a dynamic-k fixture.
Tensor identity also includes axis meaning
The port problem is easy to express:
node
+
portAttention translation adds another dimension.
A tensor’s shape may contain the same numbers while assigning them to different semantic axes.
Consider two layouts.
Batch-major SDPA layout
[B, L, H, S]where:
B
→ batch
L
→ token length
H
→ attention heads
S
→ head sizeCanonical SDPA compute layout
[B, H, L, S]Now consider PagedAttention lowering.
The token count may move into the leading dimension:
[L, 1, H, S]The buffer may contain the same element sequence.
The axis contract has changed.
A reshape that pins the leading dimension to 1 can silently move token identity to another axis.
That can produce a graph that is numerically shaped but semantically wrong.
The two GGUF decoder paths deliver attention tensors differently
The frontend supports an existing llama.cpp cgraph decoder and is being prepared for the separate native .gguf builder.
The FLASH_ATTN_EXT translator distinguishes their layouts through op_case.
Cgraph decoder: op_case == 0
The Q, K, and V tensors already arrive in:
[B, H, L, S]This is the canonical layout expected by OpenVINO SDPA.
The head axis is:
axis 1Native builder convention: op_case == 100
The tensors arrive in ggml-natural layout:
[B, L, H, S]The head axis is:
axis 2The translator first expands K/V heads along that axis, then transposes Q, K, and V into:
[B, H, L, S]before constructing SDPA.
The same operation name therefore cannot be translated by hard-coding:
heads are always dimension 1Layout is part of the decoder contract.
GQA head tiling must follow the actual head axis
Grouped-query attention can have:
number of query heads
>
number of key/value headsK and V must be repeated to match the number of query heads.
Conceptually:
Hq = 8
Hkv = 2
repeat factor = 4But the axis to repeat depends on the incoming layout.
[B, Hkv, L, S]
→ head axis 1[B, L, Hkv, S]
→ head axis 2PR #37435 derives:
head_axisfrom the decoder path and places the temporary repeat axis immediately after it.
The resulting reshape also preserves leading dimensions through special_zero rather than replacing them with static assumptions.
Live stateful tensor shapes may be dynamic even when head facts are static
On a stateful KV-cache path, the OpenVINO node feeding K or V may come from:
ReadValue
+
ConcatIts batch or sequence dimensions can be dynamic.
The number of heads and head size are still model facts known to the GGUF decoder.
The translator therefore uses:
context.get_input_shape(...)from the decoder for head count and head size instead of relying solely on the live OpenVINO node’s inferred partial shape.
Live OV tensor
batch / sequence
→ dynamicDecoder-declared ggml shape
head count / head size
→ staticThe frontend combines those two forms of knowledge rather than demanding that the runtime state node make every dimension static.
A literal 1 can destroy PagedAttention layout
The attention split reshape converts a flat projection into heads.
In batch-major SDPA execution:
input:
[1, 1, T, H*S]
output:
[1, T, H, S]A reshape target such as:
[1, -1, H, S]looks correct.
After SDPAToPagedAttention, however, the same logical activation can be token-major:
input:
[T, 1, 1, H*S]If the reshape still pins the first dimension to 1, the token count can be folded into another dimension.
The expected result is:
[T, 1, H, S]not:
[1, T, H, S]The solution is OpenVINO’s special_zero reshape behavior.
0 in target shape
→ copy corresponding input dimensionThe pattern becomes:
[0, -1, H, S]with:
special_zero = trueNow the same reshape works for both layouts.
Input [1, 1, T, H*S]
→ Output [1, T, H, S]Input [T, 1, 1, H*S]
→ Output [T, 1, H, S]The leading dimension stays where the active attention backend placed it.
The reverse reshape must preserve the token axis too
After attention, the head dimensions are merged again.
Batch-major form
[1, T, H, S]
↓
[1, 1, T, H*S]Token-major form
[T, 1, H, S]
↓
[T, 1, 1, H*S]The patched target again begins with:
0so the first input dimension is copied.
The translator also retains rank 4.
That rank is important because the next operation may be a residual Add against another rank-4 activation.
OpenVINO broadcasts elementwise operands from the right.
A rank-3 result can right-align its dimensions incorrectly and produce a token-by-token outer broadcast rather than one residual addition per token.
The source explicitly identifies the potential:
T × Tbroadcast error when the token dimension appears on an unexpected axis.
The regression test executes both physical layouts
The patch adds paired tests for reshape cases 1 and 2.
The test supplies the same flat values under:
batch-major inputand:
token-major inputIt then verifies:
correct output shape for each layout
and
identical output element orderFor the split-heads case:
Batch-major:
[1, 1, T, H*S]
→ [1, T, H, S]PagedAttention layout:
[T, 1, 1, H*S]
→ [T, 1, H, S]For the merge-heads case:
Batch-major:
[1, T, H, S]
→ [1, 1, T, H*S]PagedAttention layout:
[T, 1, H, S]
→ [T, 1, 1, H*S]The comments state that these tests fail when the target shape pins a literal leading 1.
The same buffer can have more than one valid layout interpretation
The tests emphasize that the data do not need to move.
Same contiguous values
different shape metadata
different axis ownershipThe reshape changes how the runtime interprets the buffer.
This makes metadata correctness as important as data correctness.
No copy occurreddoes not imply:
No semantic change occurredA metadata-only reshape can determine which tokens belong to which batch and which values belong to which head.
PERMUTE case 4 had to preserve static head dimensions
Another issue appeared when an active sequence count was available as a runtime Parameter.
The original reshape pattern combined:
dynamic n_seq_active
-1
static n_heads
static head_sizethrough one Concat.
Although the final two values were constants, the combined shape expression could become insufficiently bounded for downstream shape inference.
The head size could reach SDPA as dynamic.
The GPU plugin could then fail to retain the optimized SDPA representation and decompose it into:
Gemm
+
SoftMaxPR #37435 split the metadata transformation into two reshapes.
First reshape: resolve the dynamic sequence grouping
The first target is constructed from:
n_seq_active
-1Conceptually:
flat projection
↓
reshape according to active sequence countThis step handles the runtime-dependent part of the layout.
Second reshape: restore the static head structure
The second target is a fully constant pattern:
[0, -1, n_heads, head_size]The leading zero copies the already-established first dimension.
The final two dimensions remain statically visible:
n_heads
head_sizeto shape inference and backend pattern matching.
Dynamic sequence metadata
→ handled first
Static head metadata
→ reintroduced through constant patternThe code comments note that both reshapes are metadata-only, so separating them does not require an additional tensor copy.
One reshape was mathematically sufficient but compiler-inferentially weaker
From a pure element-count perspective:
one reshapeand:
two consecutive reshapescan be equivalent.
The compiler does not consume only element counts.
It also consumes:
which shape values are constant
which are dynamic graph expressions
which dimensions remain provably static
which patterns backend matchers can recognizeThe two-step version preserves stronger static knowledge.
Same numerical layout
+
better shape evidence
→ different backend lowering opportunityThis is another reason compiler frontends cannot be judged solely by final mathematical equivalence.
The PERMUTE tests pin the split-head semantics
PR #37435 added explicit tests for:
op_case 1
→ plain head/token axis swapand:
op_case 4
→ split flat projection into heads
→ then swap head and token axesFor case 4:
input:
[1, 1, token=2, heads*size=6]
output:
[1, heads=3, token=2, size=2]The test fills the input with a known increasing sequence and checks the exact output order.
It also adds a negative case verifying that an unsupported op_case throws rather than silently emitting an approximate graph.
Mathematically identical constants can produce different fusion graphs
For the builder-style attention layout, Q, K, and V each require the same transpose order:
[0, 2, 1, 3]A natural graph construction might create one shared constant:
order = Constant([0, 2, 1, 3])
Transpose(Q, order)
Transpose(K, order)
Transpose(V, order)The three transposes are mathematically correct.
But the GPU plugin’s TransposeSDPAMatcher requires the order constant to have:
consumers_count == 1A single shared constant has three consumers.
The matcher can fail to absorb the transpose pattern.
The permutes can remain in the decode path and prevent the broadcast-to-SDPA fusion.
PR #37435 therefore creates a separate but numerically identical order constant for every transpose.
Constant Q
→ one consumer
Constant K
→ one consumer
Constant V
→ one consumerThe values are the same.
The graph topology is different.
Common-subexpression sharing is not always optimization-safe
Compiler engineering often encourages deduplication.
Same constant value
→ share one nodeThis can reduce graph size.
But a pattern matcher may define its contract through node ownership or consumer count.
Shared constant
→ three consumers
→ pattern rejectedThree equivalent constants
→ one consumer each
→ pattern acceptedNeither graph changes the transpose arithmetic.
Only one preserves the backend’s expected pattern identity.
Graph equivalence for mathematics is not automatically graph equivalence for optimization.
Backend matchers consume topology as evidence
The matcher does not execute the graph to discover that the transposes are harmless.
It recognizes a structural pattern.
That pattern may require:
specific operation types
specific constant values
specific consumer relationships
no unexpected operations between nodesA frontend that inserts a mathematically neutral node or shares a constant differently can make the optimized path unreachable.
This can turn:
fused attentioninto:
separate transpose
broadcast
matmul
softmax
matmulwithout changing the model’s source-level formula.
A no-op Convert could also disable PagedAttention
The FLASH_ATTN_EXT translator uses ConvertLike to make K and V match Q’s element type.
After lowering, some of those conversions are no-ops because the tensors already share the same precision.
Numerically:
f16
→ Convert to f16
→ same valuesIt may seem harmless to leave such a node in the graph.
The state-management matcher expects the KV-cache path to have no Convert between:
cache Concatand:
SDPAA same-type conversion can therefore break the pattern and silently disable PagedAttention.
PR #37435 runs:
ConvertConvertLike
then
EliminateConvertso redundant same-type conversions are removed before backend pattern matching.
Numerical no-ops are not necessarily compiler no-ops
A node may satisfy:
output values
=
input valueswhile still changing:
graph topology
matcher reachability
ownership boundaries
fusion eligibilityExamples in this patch include:
shared versus private transpose constants
same-type Convert versus no Convert
one combined reshape versus two metadata-only reshapesAll can preserve numerical meaning.
They do not preserve the same optimization graph.
SDPA and PagedAttention need one layout-polymorphic frontend graph
The frontend cannot assume that the graph will always remain in its initial SDPA form.
A later transformation may convert it into PagedAttention.
The same translator output must therefore preserve enough information for both.
Initial SDPA graph
[B, L, H, S]
or
[B, H, L, S]PagedAttention-transformed graph
token count may move into leading dimensionIf the frontend hard-codes:
batch dimension = 1inside every reshape, it may be correct before the transformation and wrong afterward.
The patch instead copies the active leading dimension.
shape target begins with 0
special_zero = trueThe frontend does not need to know whether that leading dimension currently means:
batchor:
tokensIt preserves the caller’s current layout contract.
Layout polymorphism is not fully dynamic shape support
The patch should not be described as making all GGUF attention tensors arbitrarily dynamic.
It preserves a specific family of layouts.
Batch-major SDPA
and
token-major PagedAttentionOther facts remain static or constrained.
rank
→ often expected to be 4
head count
→ decoder-provided static fact
head size
→ decoder-provided static fact
supported op_case
→ explicitly enumeratedThe graph is flexible at the axes that need to move.
It remains strict where backend lowering requires static information.
Dynamic does not mean every dimension should become unknown
The PERMUTE fix illustrates this clearly.
The sequence count is dynamic.
The head layout is static.
A single dynamic shape expression could erase both distinctions.
The two-step reshape preserves:
sequence-related dimensions
→ runtime-dependenthead count and head size
→ compile-time constantsThis is stronger than labeling the entire tensor “dynamic.”
A good shape system preserves the maximum safe amount of knowledge.
Output ports, axes, and matcher topology are the same class of problem
At first, these fixes appear unrelated.
TopK port 1
dynamic k
special_zero reshape
static head size
private transpose constants
redundant Convert eliminationThey share one underlying contract.
A frontend must preserve every fact that later compilation stages use to determine meaning.
Port identity
Which tensor left the producer?Axis identity
Which dimension represents tokens or heads?Static-shape identity
Which dimensions are proven constants?Pattern identity
Which exact graph structure can a backend recognize?Losing any one can produce a different compiled system.
A graph edge is more than its producer name
The TopK case shows:
producer node alone
→ insufficientThe complete tensor identity is:
producer
+
port
+
element type
+
shapeThe attention cases add:
axis semantics
+
layout generation
+
backend pattern roleThe frontend is responsible for transporting that identity from the source model into OpenVINO IR.
A shape is more than a list of integers
Consider:
[1, T, H, S]and:
[T, 1, H, S]They contain the same dimensions.
They do not assign the same meaning to those dimensions.
Likewise:
[1, -1, H, S]and:
[0, -1, H, S]
with special_zeromay produce the same result for a batch-major input.
Only the second preserves a token-major leading dimension.
A test using only:
T = 1could allow both to appear correct.
The added layout tests use:
T = 2so moving the token count becomes visible.
Degenerate dimensions hide layout errors
Dimensions equal to one are especially dangerous.
batch = 1
token count = 1
number of KV heads = 1can make several distinct layouts numerically indistinguishable.
For example:
[1, 1, H, S]can be interpreted under multiple axis conventions without changing the shape tuple.
Once the token count becomes greater than one:
T = 2incorrect broadcasting or axis movement appears.
This is why the new tests use nontrivial token and head counts.
A correct frontend must preserve downstream optimization eligibility
It is possible to produce a graph that is numerically correct but operationally much worse.
SDPA pattern recognized
→ fused attention or PagedAttention pathExtra Convert or unmatched transpose topology
→ decomposition into smaller operationsThe frontend’s responsibility is not necessarily to guarantee one backend’s ultimate performance.
But when it knowingly emits an equivalent graph, it should avoid destroying a supported optimization pattern without reason.
PR #37435 makes several such patterns explicit through code comments and tests.
What PR #37435 directly changed in this area
The merged patch directly changed or added:
A shared make_topk_indices() helper
Explicit return of TopK output(1)
Port-preserving shape queries
A structural output-port regression test
Dynamic graph construction for TOP_K k
Shared TopK-based logic for ARGSORT
Static-head preservation in PERMUTE op_case 4
Layout-polymorphic RESHAPE cases 1 and 2
Separate attention handling for cgraph and builder layouts
Head-axis-dependent GQA tiling
One transpose-order Constant per Q/K/V transpose
Removal of redundant same-type Converts before state-management matching
Tests for TopK index sets
Tests for PERMUTE head splitting
Tests for both batch-major and token-major attention reshapesThe PR summary groups these under translator correctness and layout robustness, and reports 137 passing GGUF frontend tests.
What the patch does not prove
The merged evidence does not establish that:
Every multi-output translator in OpenVINO is free of port-loss bugs.Every possible dynamic TOP_K shape has a dedicated runtime regression.Every GGUF attention architecture reaches PagedAttention.The GPU plugin always fuses every graph using these shapes.Every plugin requires private transpose constants.All mathematically redundant nodes must always be removed.The separate native .gguf builder is already merged.The translator changes provide a published end-to-end latency improvement.The supported conclusion is narrower:
The GGUF frontend now preserves several graph facts that were previously easy to erase: the selected output port, runtime-derived
k, active token-axis placement, static head dimensions, and the topology expected by attention matchers.
The native builder remains a separate evidence layer
PR #37435 was merged as groundwork for the existing decoder-based frontend path.
The separate native .gguf builder remains in PR #37421.
Some op_case comments describe how the native builder is expected to present tensors, and the merged translator now understands those conventions.
That does not mean direct .gguf file ingestion itself was merged by #37435.
The distinction remains:
Merged
decoder-based frontend groundwork
+
translator corrections
+
stateful transformation
+
testsSeparate open work
native .gguf container parsing
+
architecture-specific graph constructionThis boundary was stated directly in the PR description.
The deeper lesson is that frontends preserve contracts, not just operations
A superficial translator can map:
GGML TOP_K
→ OpenVINO TopKand appear complete.
A correct translator must ask:
Which TopK output?
Which index type?
How is k obtained?
Which axis is sorted?
What result ordering does the source guarantee?Likewise:
GGML attention
→ OpenVINO SDPAis not enough.
The translator must preserve:
head axis
token axis
batch or token-major layout
static head size
GQA expansion axis
reshape rank
backend-recognizable topologyThe operation name is only the beginning.
A model frontend is correct when the tensor arriving at the next stage retains the same identity, axis meaning, and execution contract as the tensor described by the source graph.
A tensor’s full identity extends beyond its values
The final boundary can be written as:
Tensor identity
=
producer operation
+
output port
+
element type
+
shape
+
axis semantics
+
graph-generation roleTwo tensors can hold the same numerical values and still be different compiler objects.
Same values
+
different output port
→ different tensorSame flat buffer
+
different token axis
→ different layoutSame transpose order
+
different constant-sharing topology
→ different fusion resultSame dtype before and after Convert
+
extra node in state path
→ different matcher resultPR #37435 repaired all four kinds of boundary.
Why these errors are difficult to detect in small tests
A small test can accidentally erase the difference.
TopK values and indices
→ same shapetoken count = 1
→ batch-major and token-major shapes look similarnumber of heads = 1
→ head-axis mistakes can broadcast successfullyno backend fusion check
→ decomposed graph still produces correct valuesThe test passes.
The production graph is still wrong or slower.
The strongest tests therefore choose inputs that make the hidden distinction observable.
Inspect port index directly
Use token count greater than one
Use head count different from token count
Compare both layout forms
Check graph operation structure
Check unsupported cases failThe output-port test and layout tests prove different layers
Port test
Does ShapeOf consume TopK output 1?This is structural graph evidence.
TopK numerical test
Are the selected index sets correct?This is result evidence.
Reshape layout tests
Does the same buffer remain valid in batch-major and token-major forms?This is layout-contract evidence.
Backend-matcher comments and source structure
Does the emitted graph preserve known fusion preconditions?This is lowering-path evidence.
No single test substitutes for all four.
Part 3: why a self-written oracle repeated the same mistaken formula
The final article examines the test boundary behind PR #37435.
Before the patch, activation tests could compare a translator against a NumPy or C++ formula written by the same developers who interpreted the ggml operation.
That proves:
translator
=
test author’s formulaIt does not independently prove:
test author’s formula
=
actual ggml implementationGELU_QUICK demonstrated the failure.
Its correct ggml form is:
x * sigmoid(1.702 * x)A plausible but different GELU formula could be implemented in both:
translator
and
test oracleand the test would pass.
PR #37435 added expected output captured from real ggml execution and introduced a coverage gate that compares the registered translator table with the operations actually converted by the test binary.
Part 3 examines:
Why real ggml output is a stronger oracle
Why a test list can drift from an op table
Why the coverage gate runs during global TearDown
Why a filtered test run must disable that gate
And what 137/137 actually provesPrevious article
OpenVINO #37435 — Why GGUF Statefulness Became a Consumer Choice Instead of a Decoder Property
Related material
Patch status: Merged into OpenVINO master as commit 57070bcb
Multi-output contract: TopK output 0 is values; output 1 is indices
Port-preserving type: ov::Output<ov::Node>
Shared helper: make_topk_indices() returns TopK::output(1)
Dynamic TOP_K behavior: Uses a static constant when possible and a runtime shape-derived scalar otherwise
Attention layouts: Supports cgraph [B,H,L,S] and builder-style [B,L,H,S] inputs
PagedAttention boundary: Leading dimensions are copied with special_zero rather than pinned to literal 1
Static head boundary: Dynamic sequence reshaping is separated from a constant head-layout reshape
Fusion boundary: Q/K/V transposes use separate order constants; redundant same-type Converts are removed
Validation: ov_gguf_frontend_tests reported 137/137
Evidence limitation: No dedicated dynamic-k runtime fixture or published end-to-end performance benchmark was identified in this PR
This is Part 2 of a three-part series on OpenVINO’s GGUF frontend groundwork.
Part 1 examined why KV-cache state became a consumer-selected transformation rather than a decoder property.
This article examined why a multi-output tensor must preserve its port identity, why dynamic dimensions should remain graph values, and why attention reshapes must preserve both runtime token placement and static head information.
Part 3 examines why a translator tested against a formula written by the same author can still be wrong, how real ggml output exposed the GELU_QUICK mismatch, and how the new op-coverage gate prevents registered translators from remaining untested.
#OpenVINO #GGUF #GGML #TopK #PagedAttention #SDPA #GraphCompiler #TensorLayout #ModelFrontend #CodeAnalysis