Skip to content

Commit bad9e28

Browse files
committed
Write HF static KV cache rows at the cache_position input
With transformers 5.11.0, StaticLayer.update ignores cache_position and writes K/V at arange(n) + cumulative_length. Setting cumulative_length from cache_position keeps eager results right, but the exported cache writes then index rows with the output of an add. The ggml engine only accepts index_put_ row indices that come from a graph input or an integer arange, so updated codes Signed-off-by: jiseong.oh <jiseong.oh@samsung.com>
1 parent 104da5e commit bad9e28

6 files changed

Lines changed: 98 additions & 24 deletions

File tree

‎backends/arm/test/modules/test_moe.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ def _make_gpt_oss_moe(dtype: torch.dtype) -> tuple[GptOssMLP, GptOssConfig]:
171171
intermediate_size=16,
172172
num_local_experts=8,
173173
num_experts_per_tok=4,
174-
experts_implementation="eager",
175174
)
175+
config._experts_implementation = "eager"
176176
model = GptOssMLP(config)
177177
model.experts = TensorizedGptOssExperts(config)
178178
generator = torch.Generator().manual_seed(2648)

‎backends/native/examples/audio/export_hf_whisper.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040

4141
import torch
4242
import torch.nn as nn
43-
from executorch.backends.native.examples.hf_llm.hf_utils import report_lowering
43+
from executorch.backends.native.examples.hf_llm.hf_utils import (
44+
PositionedStaticLayer,
45+
report_lowering,
46+
)
4447
from executorch.exir.native import to_native
4548
from torch.export import Dim, export, ExportedProgram
4649
from transformers import (
@@ -174,6 +177,8 @@ def __init__(self, model: WhisperForConditionalGeneration) -> None:
174177
dtype=torch.float32,
175178
device=torch.device("cpu"),
176179
)
180+
for i, old in enumerate(self.self_cache.layers):
181+
self.self_cache.layers[i] = PositionedStaticLayer.replacing(old, old.dtype)
177182
self.cross_cache = DynamicCache(config=config)
178183
self.cache = EncoderDecoderCache(self.self_cache, self.cross_cache)
179184
for layer_idx in range(config.decoder_layers):
@@ -195,8 +200,10 @@ def forward(
195200
cache_position: torch.Tensor,
196201
*cross_kv: torch.Tensor,
197202
) -> torch.Tensor:
203+
# transformers 5 takes positions from the cache's length, not cache_position.
198204
for layer in self.self_cache.layers:
199205
layer.cumulative_length = cache_position[:1].clone()
206+
layer.positions = cache_position
200207
num_layers = len(self.cross_cache.layers)
201208
for layer_idx, layer in enumerate(self.cross_cache.layers):
202209
layer.keys = cross_kv[layer_idx]

‎backends/native/examples/hf_llm/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ fbcode_target(
1212
"//caffe2:torch",
1313
"//executorch/backends/native:lib",
1414
"//executorch/exir:lib",
15+
"fbsource//third-party/pypi/transformers:transformers",
1516
],
1617
)
1718

‎backends/native/examples/hf_llm/export_hf_llm.py‎

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
gguf_architecture,
2626
gguf_to_hf,
2727
)
28-
from executorch.backends.native.examples.hf_llm.hf_utils import report_lowering
28+
from executorch.backends.native.examples.hf_llm.hf_utils import (
29+
PositionedStaticLayer,
30+
report_lowering,
31+
)
2932
from executorch.exir.native import Call, Sample, SampleSet, to_native, write_samples
3033
from executorch.extension.llm.export.load import assign_one, iter_checkpoint, RawIter
3134
from torch.export import Dim, export
@@ -118,15 +121,6 @@ def load_gguf(
118121
_CACHE_DTYPE_SDPA = "sdpa_in_cache_dtype"
119122

120123

121-
class _CastingStaticLayer(StaticLayer):
122-
"""A static cache layer that casts K/V writes to its own dtype."""
123-
124-
def update(self, key_states, value_states, cache_kwargs=None):
125-
return super().update(
126-
key_states.to(self.dtype), value_states.to(self.dtype), cache_kwargs
127-
)
128-
129-
130124
def _sdpa_in_cache_dtype(module, query, key, value, attention_mask, **kwargs):
131125
output, weights = sdpa_attention_forward(
132126
module, query.to(key.dtype), key, value, attention_mask, **kwargs
@@ -155,22 +149,29 @@ def __init__(
155149
if hasattr(self.cached, "static_cache")
156150
else self.cached.cache
157151
)
158-
if kv_cache_dtype != model.dtype:
159-
self._store_kv_as(model, kv_cache_dtype)
152+
change_dtype = kv_cache_dtype != model.dtype
153+
self._write_cache_at_input_positions(kv_cache_dtype, change_dtype)
154+
if change_dtype:
155+
self._attend_in_cache_dtype(model)
160156

161-
def _store_kv_as(self, model: PreTrainedModel, dtype: torch.dtype) -> None:
157+
def _write_cache_at_input_positions(
158+
self, dtype: torch.dtype, change_dtype: bool
159+
) -> None:
162160
for i, old in enumerate(self.cache.layers):
163161
if type(old) is not StaticLayer:
164-
raise ValueError(f"cannot change the KV dtype of {type(old).__name__}")
165-
layer = _CastingStaticLayer(old.max_cache_len)
166-
layer.lazy_initialization(
167-
old.keys[:, :, :1].to(dtype), old.values[:, :, :1].to(dtype)
168-
)
162+
if change_dtype:
163+
raise ValueError(
164+
f"cannot change the KV dtype of {type(old).__name__}"
165+
)
166+
continue
167+
layer = PositionedStaticLayer.replacing(old, dtype)
169168
self.cache.layers[i] = layer
170169
self.cached.register_buffer(f"key_cache_{i}", layer.keys, persistent=False)
171170
self.cached.register_buffer(
172171
f"value_cache_{i}", layer.values, persistent=False
173172
)
173+
174+
def _attend_in_cache_dtype(self, model: PreTrainedModel) -> None:
174175
# Keep the vmap-free mask TorchExportableModuleForDecoderOnlyLM selected.
175176
mask = ALL_MASK_ATTENTION_FUNCTIONS[model.config._attn_implementation]
176177
AttentionInterface.register(_CACHE_DTYPE_SDPA, _sdpa_in_cache_dtype)
@@ -180,9 +181,11 @@ def _store_kv_as(self, model: PreTrainedModel, dtype: torch.dtype) -> None:
180181
def forward(
181182
self, input_ids: torch.Tensor, cache_position: torch.Tensor
182183
) -> torch.Tensor:
183-
# StaticLayer writes at cumulative_length and ignores cache_position.
184+
# transformers 5 takes positions from the cache's length, not cache_position.
184185
for layer in self.cache.layers:
185186
layer.cumulative_length = cache_position[:1].clone()
187+
if isinstance(layer, PositionedStaticLayer):
188+
layer.positions = cache_position
186189
return self.cached.model(
187190
input_ids=input_ids,
188191
cache_position=cache_position,

‎backends/native/examples/hf_llm/hf_utils.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@
1919
get_lowered_submodules,
2020
)
2121
from torch.export import ExportedProgram
22+
from transformers.cache_utils import StaticLayer
23+
24+
25+
class PositionedStaticLayer(StaticLayer):
26+
"""A static cache layer that writes K/V, cast to its own dtype, at ``positions``.
27+
28+
transformers 5 writes at ``arange(n) + cumulative_length``. The native runtime
29+
only accepts row indices that are a graph input or an arange, so the positions
30+
are the ``cache_position`` input of the graph instead.
31+
"""
32+
33+
positions: torch.Tensor
34+
35+
@classmethod
36+
def replacing(cls, old: StaticLayer, dtype: torch.dtype) -> "PositionedStaticLayer":
37+
layer = cls(old.max_cache_len)
38+
layer.lazy_initialization(
39+
old.keys[:, :, :1].to(dtype), old.values[:, :, :1].to(dtype)
40+
)
41+
return layer
42+
43+
def update(self, key_states, value_states, *args, **kwargs):
44+
self.keys.index_copy_(2, self.positions, key_states.to(self.dtype))
45+
self.values.index_copy_(2, self.positions, value_states.to(self.dtype))
46+
return self.keys, self.values
2247

2348

2449
def histogram(gm: torch.fx.GraphModule) -> collections.Counter:

‎backends/native/examples/hf_llm/test_export_hf_llm.py‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,44 @@ def test_golden_samples_tolerance_follows_weight_quantization(self) -> None:
316316
samples = golden_samples(LastTokenLM(model, 16).eval(), [[3, 4]], 0)
317317
self.assertEqual(samples.tol, tol)
318318

319-
def test_fp16_kv_cache_rejects_sliding_window_layers(self) -> None:
320-
with self.assertRaisesRegex(ValueError, "cannot change the KV dtype"):
321-
LastTokenLM(_tiny("gemma3"), 32, torch.float16)
319+
def test_cache_writes_index_the_cache_position_input(self) -> None:
320+
seq = torch.export.Dim("seq", min=1, max=31)
321+
for arch, kv_dtype in (("llama", torch.float32), ("gemma3", torch.float16)):
322+
with self.subTest(arch=arch, kv_dtype=kv_dtype):
323+
torch.manual_seed(7)
324+
model = _tiny(arch)
325+
fp32 = LastTokenLM(model, 32).eval()
326+
wrapper = LastTokenLM(copy.deepcopy(model), 32, kv_dtype).eval()
327+
ids = torch.tensor([[3, 4, 5, 6]])
328+
with torch.no_grad():
329+
ep = torch.export.export(
330+
wrapper,
331+
(ids, torch.arange(4)),
332+
dynamic_shapes={
333+
"input_ids": {1: seq},
334+
"cache_position": {0: seq},
335+
},
336+
strict=True,
337+
)
338+
exported = ep.module()
339+
for step_ids, positions in (
340+
(ids, torch.arange(4)),
341+
(torch.tensor([[9]]), torch.tensor([4])),
342+
(ids, torch.arange(4)),
343+
):
344+
self.assertTrue(
345+
torch.allclose(
346+
fp32(step_ids, positions),
347+
exported(step_ids, positions),
348+
atol=1e-2,
349+
rtol=2e-2,
350+
)
351+
)
352+
writes = [
353+
n
354+
for n in ep.graph.nodes
355+
if n.target == torch.ops.aten.index_copy_.default
356+
]
357+
self.assertEqual(len(writes), 2 * model.config.num_hidden_layers)
358+
for n in writes:
359+
self.assertEqual(n.args[2].name, "cache_position")

0 commit comments

Comments
 (0)