Skip to content

Commit 5529555

Browse files
committed
Adapt draft to HF Qwen3 modules, dynamic block_len, draft KV cache
1 parent 1f0894b commit 5529555

7 files changed

Lines changed: 424 additions & 182 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Persistent per-layer KV cache for the DFlash draft model.
8+
9+
Follows the TorchExportableModuleWithStaticCache pattern (per review): cache
10+
tensors are registered as mutable buffers and cache_position is passed
11+
through the call rather than tracked internally. Built on the existing
12+
KVCache (backends/mlx/llm/cache.py), so writes go through
13+
torch.ops.mlx.kv_cache_update instead of a Python slice (avoids the
14+
GuardOnDataDependentSymNode failure hit by an earlier attempt).
15+
"""
16+
17+
from typing import Tuple, Union
18+
19+
import torch
20+
import torch.nn as nn
21+
22+
from executorch.backends.mlx.llm.cache import KVCache
23+
24+
25+
class DFlashDraftKVCache(nn.Module):
26+
def __init__(
27+
self,
28+
num_layers: int,
29+
num_heads: int,
30+
head_dim: int,
31+
max_seq_len: int,
32+
dtype: torch.dtype = torch.float32,
33+
):
34+
super().__init__()
35+
self.max_seq_len = max_seq_len
36+
self.layers = nn.ModuleList(
37+
[
38+
KVCache(
39+
max_batch_size=1,
40+
max_context_length=max_seq_len,
41+
n_heads=num_heads,
42+
head_dim=head_dim,
43+
enable_dynamic_shape=True,
44+
dtype=dtype,
45+
)
46+
for _ in range(num_layers)
47+
]
48+
)
49+
50+
def write(
51+
self,
52+
layer_idx: int,
53+
key_states: torch.Tensor,
54+
value_states: torch.Tensor,
55+
cache_position: Union[torch.Tensor, int],
56+
) -> Tuple[torch.Tensor, torch.Tensor]:
57+
# Write K/V at cache_position and return the FULL buffer for this
58+
# layer. Caller masks the unwritten tail via valid_mask(). Extract
59+
# cache_position once and reuse across layers, not once per layer.
60+
return self.layers[layer_idx].update(cache_position, key_states, value_states)
61+
62+
def valid_mask(self, valid_len: Union[torch.Tensor, int], device=None) -> torch.Tensor:
63+
# True for positions [0, valid_len), False for the unwritten tail.
64+
positions = torch.arange(self.max_seq_len, device=device)
65+
return positions < valid_len
66+
67+
def reset(self) -> None:
68+
for layer in self.layers:
69+
layer.k_cache.zero_()
70+
layer.v_cache.zero_()

0 commit comments

Comments
 (0)