Skip to content

Commit 6f31ee3

Browse files
committed
Plumb transposed cache config through export pipeline
Benchmarking shows transposed KV cache [B, H, S, D] significantly outperforms standard layout [B, S, H, D] in custom_sdpa, especially at longer cache fills: 1.64x at start_pos=1024, 1.14x at start_pos=512, 1.13x for prefill seq_len=512 (Llama 3 8B config, Apple M-series). The improvement comes from better memory locality in the attn_score @ V GEMM where V stride along S_kv changes from H*D to D. This commit replaces the hardcoded `is_seq_at_dim_2=True # hacking temporarily` values in sdpa.py and custom_kv_cache.py with a proper configurable parameter threaded through the export pipeline: - Add `use_transposed_cache: bool = True` to ModelConfig in llm_config.py - Thread it through _get_source_transforms in export_llama_lib.py - Add `is_seq_at_dim_2` parameter to replace_kv_cache_with_custom_kv_cache and replace_sdpa_with_custom_op (defaulting to True for backward compat) Also fixes: - torchao aarch64:matmul BUCK: deps -> exported_deps for :macro, fixing transitive header visibility on arm64 - op_update_cache.cpp: %zd -> PRId64 for int64_t format strings Authored with Claude. Differential Revision: [D99677679](https://our.internmc.facebook.com/intern/diff/D99677679/) [ghstack-poisoned]
1 parent 2bd6e06 commit 6f31ee3

5 files changed

Lines changed: 32 additions & 21 deletions

File tree

examples/models/llama/export_llama_lib.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ def _prepare_for_llama_export(llm_config: LlmConfig) -> LLMEdgeManager:
765765
llm_config.model, "use_custom_sdpa_with_attention_mask", False
766766
),
767767
use_sdpa_with_kv_cache=llm_config.model.use_sdpa_with_kv_cache,
768+
use_transposed_cache=llm_config.model.use_transposed_cache,
768769
quantize_kv_cache=llm_config.model.quantize_kv_cache,
769770
use_kv_cache=llm_config.model.use_kv_cache,
770771
qnn=llm_config.backend.qnn.enabled,
@@ -1603,6 +1604,7 @@ def _get_source_transforms( # noqa
16031604
expand_rope_table: bool = False,
16041605
use_custom_sdpa_with_attention_mask: bool = False,
16051606
use_sdpa_with_kv_cache: bool = False,
1607+
use_transposed_cache: bool = True,
16061608
quantize_kv_cache: bool = False,
16071609
use_kv_cache: bool = False,
16081610
qnn: bool = False,
@@ -1639,6 +1641,7 @@ def _get_source_transforms( # noqa
16391641
expand_rope_table: Whether to expand rope table.
16401642
use_custom_sdpa_with_attention_mask: Whether to use custom SDPA with attention mask.
16411643
use_sdpa_with_kv_cache: Whether to use SDPA with KV cache.
1644+
use_transposed_cache: Whether to store KV cache in transposed layout [B, H, S, D].
16421645
quantize_kv_cache: Whether to quantize KV cache.
16431646
use_kv_cache: Whether to use KV cache.
16441647
qnn: Whether to use QNN.
@@ -1734,16 +1737,20 @@ def _get_source_transforms( # noqa
17341737
use_attention_mask_for_custom_sdpa = use_custom_sdpa_with_attention_mask
17351738

17361739
if use_sdpa_with_kv_cache:
1737-
transforms.append(replace_kv_cache_with_custom_kv_cache)
1740+
transforms.append(
1741+
partial(replace_kv_cache_with_custom_kv_cache, is_seq_at_dim_2=use_transposed_cache)
1742+
)
17381743
# todo: do this optionally
17391744
# if use attention mask instead of causal attention
17401745
# then create partial function that sets use_attention_mask=True
17411746
if use_attention_mask_for_custom_sdpa:
17421747
transforms.append(
1743-
partial(replace_sdpa_with_custom_op, use_attention_mask=True)
1748+
partial(replace_sdpa_with_custom_op, use_attention_mask=True, is_seq_at_dim_2=use_transposed_cache)
17441749
)
17451750
else:
1746-
transforms.append(replace_sdpa_with_custom_op)
1751+
transforms.append(
1752+
partial(replace_sdpa_with_custom_op, is_seq_at_dim_2=use_transposed_cache)
1753+
)
17471754

17481755
if quantize_kv_cache:
17491756
assert use_kv_cache, "quantize_kv_cache requires use_kv_cache=True"

examples/models/llama/source_transformation/custom_kv_cache.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -391,21 +391,20 @@ def update(
391391
return (self.k_cache, self.v_cache)
392392

393393

394-
def replace_kv_cache_with_custom_kv_cache(module):
394+
def replace_kv_cache_with_custom_kv_cache(module, is_seq_at_dim_2=True):
395395
"""
396396
Replace KVCache with CustomKVCache. This modifies the model in place.
397-
At the moment custom kv cache only supports cache with shape
398-
[B, S, H, D] as opposed to [B, H, S, D]
399-
This is because the custom op treats second dim as sequence dim.
400-
Future work: support [B, H, S, D]
397+
When is_seq_at_dim_2=True, cache is stored as [B, H, S, D] (transposed),
398+
which improves SDPA GEMM performance via better memory locality.
399+
When is_seq_at_dim_2=False, cache is stored as [B, S, H, D] (standard).
401400
"""
402401
logging.info(
403402
"Replacing KVCache with CustomKVCache. This modifies the model in place."
404403
)
405-
return _replace_kv_cache_with_custom_kv_cache(module)
404+
return _replace_kv_cache_with_custom_kv_cache(module, is_seq_at_dim_2=is_seq_at_dim_2)
406405

407406

408-
def _replace_kv_cache_with_custom_kv_cache(module):
407+
def _replace_kv_cache_with_custom_kv_cache(module, is_seq_at_dim_2=True):
409408
for name, child in module.named_children():
410409
if isinstance(child, KVCache):
411410
cache_dtype = child.k_cache.dtype
@@ -422,11 +421,11 @@ def _replace_kv_cache_with_custom_kv_cache(module):
422421
n_heads,
423422
head_dim,
424423
dtype=cache_dtype,
425-
is_seq_at_dim_2=True, # hacking temporarily
424+
is_seq_at_dim_2=is_seq_at_dim_2,
426425
),
427426
)
428427
else:
429-
_replace_kv_cache_with_custom_kv_cache(child)
428+
_replace_kv_cache_with_custom_kv_cache(child, is_seq_at_dim_2=is_seq_at_dim_2)
430429
return module
431430

432431

examples/models/llama/source_transformation/sdpa.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def forward(
8282

8383

8484
def _replace_sdpa_with_custom_op(
85-
module: torch.nn.Module, use_attention_mask: bool = False
85+
module: torch.nn.Module, use_attention_mask: bool = False, is_seq_at_dim_2: bool = True
8686
):
8787
for name, child in module.named_children():
8888
if isinstance(child, SDPA):
@@ -92,19 +92,19 @@ def _replace_sdpa_with_custom_op(
9292
SDPACustom(
9393
child.dim,
9494
use_attention_mask=use_attention_mask,
95-
is_seq_at_dim_2=True, # hacking temporarily
95+
is_seq_at_dim_2=is_seq_at_dim_2,
9696
),
9797
)
9898
else:
99-
_replace_sdpa_with_custom_op(child, use_attention_mask=use_attention_mask)
99+
_replace_sdpa_with_custom_op(child, use_attention_mask=use_attention_mask, is_seq_at_dim_2=is_seq_at_dim_2)
100100

101101

102102
def replace_sdpa_with_custom_op(
103-
module: torch.nn.Module, use_attention_mask: bool = False
103+
module: torch.nn.Module, use_attention_mask: bool = False, is_seq_at_dim_2: bool = True
104104
) -> torch.nn.Module:
105105
from executorch.extension.llm.custom_ops import custom_ops # noqa
106106

107-
_replace_sdpa_with_custom_op(module, use_attention_mask=use_attention_mask)
107+
_replace_sdpa_with_custom_op(module, use_attention_mask=use_attention_mask, is_seq_at_dim_2=is_seq_at_dim_2)
108108
return module
109109

110110

extension/llm/custom_ops/op_update_cache.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,17 @@ Tensor& update_cache_impl(
119119

120120
ET_CHECK_MSG(
121121
value_batch_size == cache_batch_size,
122-
"projected_value batch size (%zd) should be equal to the cache batch size (%zd).",
122+
"projected_value batch size (%" PRId64 ") should be equal to the cache batch size (%" PRId64 ").",
123123
value_batch_size,
124124
cache_batch_size);
125125
ET_CHECK_MSG(
126126
value_num_heads == cache_num_heads,
127-
"projected_value number of heads (%zd) should be equal to the cache number of heads (%zd).",
127+
"projected_value number of heads (%" PRId64 ") should be equal to the cache number of heads (%" PRId64 ").",
128128
value_num_heads,
129129
cache_num_heads);
130130
ET_CHECK_MSG(
131131
value_head_dim == cache_head_dim,
132-
"projected_value embedding dimension (%zd) should be equal to the cache embedding dimension (%zd).",
132+
"projected_value embedding dimension (%" PRId64 ") should be equal to the cache embedding dimension (%" PRId64 ").",
133133
value_head_dim,
134134
cache_head_dim);
135135
ET_CHECK_MSG(
@@ -210,7 +210,7 @@ Tensor& update_cache_impl(
210210
// Ensure the target position is valid
211211
ET_CHECK_MSG(
212212
target_pos >= 0 && target_pos < cache_seq_len,
213-
"Index out of bounds: %" PRId64 " not in [0, %zd)",
213+
"Index out of bounds: %" PRId64 " not in [0, %" PRId64 ")",
214214
target_pos,
215215
cache_seq_len);
216216

extension/llm/export/config/llm_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ class ModelConfig:
174174
use_sdpa_with_kv_cache: Whether to use flash attention by substituting
175175
for our custom SDPA op. Note that the naming is poor and this
176176
doesn't actually have anything to do with the kv_cache at the moment.
177+
use_transposed_cache: Whether to store KV cache in transposed layout
178+
[B, H, S, D] instead of standard [B, S, H, D]. Transposed layout
179+
improves SDPA performance by enabling contiguous memory access in
180+
the attn_score @ V GEMM (stride D instead of H*D along seq dim).
177181
expand_rope_table: Temporary workaround to expand sin/cos table in head
178182
dim to take vectorized path in optimized kernels.
179183
use_attention_sink: Whether to use attention sink to support multi-round
@@ -194,6 +198,7 @@ class ModelConfig:
194198
enable_dynamic_shape: bool = True
195199
use_shared_embedding: bool = False
196200
use_sdpa_with_kv_cache: bool = False
201+
use_transposed_cache: bool = True
197202
expand_rope_table: bool = False
198203
use_attention_sink: Optional[str] = None
199204
output_prune_map: Optional[str] = None

0 commit comments

Comments
 (0)