TensorTonic
Problems
Paged KV Cache Gather
JAX
Medium

Gather the cached keys and values of every sequence in a batch from a shared pool of fixed-size pages, the way paged attention stores its KV cache.

The pool k_pages has shape (num_pages, page_size, num_heads, head_dim). Page ppp holds page_size consecutive tokens, and its slot sss holds the key vectors of one token for every head. v_pages has the same shape and layout for values. The batch holds B sequences, one per row of block_tables. Sequence bbb splits its tokens into blocks of SSS = page_size tokens, and row bbb of block_tables lists the page that stores each block. Token position jjj of sequence bbb therefore lives in block ⌊j/S⌋\lfloor j / S \rfloor⌊j/S⌋, at slot j mod Sj \bmod SjmodS of that block's page.

keys[b,j]={k_pages[block_tables[b,⌊j/S⌋], j mod S]if j<seq_lens[b]0otherwise\text{keys}[b, j] = \begin{cases} \text{k\_pages}\big[\text{block\_tables}[b, \lfloor j / S \rfloor],\ j \bmod S\big] & \text{if } j < \text{seq\_lens}[b] \\ 0 & \text{otherwise} \end{cases}keys[b,j]={k_pages[block_tables[b,⌊j/S⌋], jmodS]0​if j<seq_lens[b]otherwise​

Here seq_lens[b] is the number of tokens sequence bbb has cached, and jjj runs from 0 to L−1L - 1L−1 with LLL = max_blocks times page_size, where max_blocks is the width of block_tables. values follows the same rule with v_pages. Entries of block_tables past the blocks a sequence uses are -1, and slots past the end of a sequence inside its last page still hold old data from earlier requests, so none of them may reach the output.

Return a dict with three keys. keys and values are JAX arrays of shape (B, L, num_heads, head_dim) with the dtype of k_pages and v_pages, and mask is a bool JAX array of shape (B, L) that is True exactly at the positions j<j <j< seq_lens[b].

Examples

Input: k_pages = [[[[-1.6,-1.5]],[[1.3,-0.7]]],[[[-1.0,-2.0]],[[-0.1,0.5]]],[[[0.7,-1.2]],[[-1.5,-1.5]]],[[[-0.8,0.3]],[[-1.8,0.9]]]], v_pages = [[[[0.6,0.8]],[[-0.9,-0.4]]],[[[1.4,-0.5]],[[-0.9,0.8]]],[[[1.2,1.1]],[[-1.4,0.2]]],[[[-1.8,-0.3]],[[1.9,-0.9]]]], block_tables = [[2,0]], seq_lens = [3]

Output: {"keys":[[[[0.7,-1.2]],[[-1.5,-1.5]],[[-1.6,-1.5]],[[0.0,0.0]]]],"values":[[[[1.2,1.1]],[[-1.4,0.2]],[[0.6,0.8]],[[0.0,0.0]]]],"mask":[[true,true,true,false]]}

Explanation: With page_size 2, positions 0 and 1 form block 0, which lives in page 2, so they copy slots 0 and 1 of page 2, the keys [0.7, -1.2] and [-1.5, -1.5]. Position 2 is slot 0 of block 1, which lives in page 0, so it copies [-1.6, -1.5]. Position 3 is past seq_lens = 3, so it is zero even though slot 1 of page 0 holds [1.3, -0.7].

Input: k_pages = [[[[-1.9,0.5]],[[-0.8,0.2]]],[[[-1.7,0.9]],[[-0.8,2.0]]],[[[-0.9,-1.5]],[[-1.5,-0.7]]],[[[1.4,-0.5]],[[-1.1,-0.3]]],[[[0.1,-1.1]],[[0.7,-2.0]]],[[[0.5,0.6]],[[1.5,1.1]]]], v_pages = [[[[1.4,-0.8]],[[1.1,-1.8]]],[[[-0.8,-1.1]],[[1.4,-1.8]]],[[[-1.6,-0.5]],[[-0.2,-0.4]]],[[[-1.6,-0.2]],[[-0.4,0.5]]],[[[-1.6,-0.6]],[[1.7,-1.4]]],[[[0.1,1.9]],[[-0.5,-1.8]]]], block_tables = [[4,5,1],[3,-1,-1]], seq_lens = [5,2]

Output: {"keys":[[[[0.1,-1.1]],[[0.7,-2.0]],[[0.5,0.6]],[[1.5,1.1]],[[-1.7,0.9]],[[0.0,0.0]]],[[[1.4,-0.5]],[[-1.1,-0.3]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]]],"values":[[[[-1.6,-0.6]],[[1.7,-1.4]],[[0.1,1.9]],[[-0.5,-1.8]],[[-0.8,-1.1]],[[0.0,0.0]]],[[[-1.6,-0.2]],[[-0.4,0.5]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]]],"mask":[[true,true,true,true,true,false],[true,true,false,false,false,false]]}

Input: k_pages = [[[[1.9,1.6]],[[-0.9,-1.3]],[[-2.0,-1.0]]],[[[-1.8,-1.5]],[[-0.1,0.6]],[[-1.6,0.2]]],[[[-0.7,1.7]],[[0.5,-0.7]],[[1.2,-1.9]]]], v_pages = [[[[0.9,-1.6]],[[1.4,1.4]],[[-1.7,-1.3]]],[[[-0.8,-1.5]],[[-1.1,1.6]],[[-0.5,1.2]]],[[[1.5,-1.1]],[[-0.2,0.3]],[[-1.7,0.9]]]], block_tables = [[-1],[1]], seq_lens = [0,3]

Output: {"keys":[[[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]],[[[-1.8,-1.5]],[[-0.1,0.6]],[[-1.6,0.2]]]],"values":[[[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]],[[[-0.8,-1.5]],[[-1.1,1.6]],[[-0.5,1.2]]]],"mask":[[false,false,false],[true,true,true]]}

Hint 1

pos = jnp.arange(L); pos // page_size picks the block_tables column and pos % page_size the slot.

Hint 2

k_pages[pages, slots] with pages of shape (B, L) and slots of shape (L,) gathers every position at once.

Hint 3

jnp.where(mask[:, :, None, None], gathered, 0) zeroes padding and keeps the dtype.

Requirements

  • The function is compiled with jax.jit and every argument is traced, so the sequence lengths and page indices are known only as traced arrays. Python loops or if statements that depend on their values fail.

Constraints

  • num_pages is between 3 and 12, page_size is between 1 and 4, num_heads is between 1 and 2, and head_dim is between 1 and 3.
  • B is between 1 and 4 and max_blocks is between 1 and 3.
  • k_pages and v_pages share one shape, are float32 or bfloat16, and hold finite values.
  • block_tables is an int32 array of shape (B, max_blocks). For each sequence its first ceil(seq_lens[b] / page_size) entries are valid page indices that no other sequence uses, and its remaining entries are -1.
  • seq_lens is an int32 array of shape (B,) with values from 0 to max_blocks times page_size.
Try Similar Problems
Greedy Decode Loop with ScanMediumBeam Search StepHardSpeculative Decoding AcceptanceHardPad and Mask to Bucket LengthMediumHessian-Vector ProductMediumPer-Example Gradient ClippingMediumStraight-Through Estimator with Custom VJPMediumTraced Learning Rate ScheduleMediumExponential Moving Average with ScanMediumSpectral Norm by Power IterationMediumBatched Attention with vmapMediumPairwise Distances with Nested vmapMedium

Accepts: array

Accepts: array

Accepts: array

Accepts: array

You must run your code first.
Paged KV Cache Gather

Paged KV Cache Gather

JAX
Medium

Gather the cached keys and values of every sequence in a batch from a shared pool of fixed-size pages, the way paged attention stores its KV cache.

The pool k_pages has shape (num_pages, page_size, num_heads, head_dim). Page ppp holds page_size consecutive tokens, and its slot sss holds the key vectors of one token for every head. v_pages has the same shape and layout for values. The batch holds B sequences, one per row of block_tables. Sequence bbb splits its tokens into blocks of SSS = page_size tokens, and row bbb of block_tables lists the page that stores each block. Token position jjj of sequence bbb therefore lives in block ⌊j/S⌋\lfloor j / S \rfloor⌊j/S⌋, at slot j mod Sj \bmod SjmodS of that block's page.

keys[b,j]={k_pages[block_tables[b,⌊j/S⌋], j mod S]if j<seq_lens[b]0otherwise\text{keys}[b, j] = \begin{cases} \text{k\_pages}\big[\text{block\_tables}[b, \lfloor j / S \rfloor],\ j \bmod S\big] & \text{if } j < \text{seq\_lens}[b] \\ 0 & \text{otherwise} \end{cases}keys[b,j]={k_pages[block_tables[b,⌊j/S⌋], jmodS]0​if j<seq_lens[b]otherwise​

Here seq_lens[b] is the number of tokens sequence bbb has cached, and jjj runs from 0 to L−1L - 1L−1 with LLL = max_blocks times page_size, where max_blocks is the width of block_tables. values follows the same rule with v_pages. Entries of block_tables past the blocks a sequence uses are -1, and slots past the end of a sequence inside its last page still hold old data from earlier requests, so none of them may reach the output.

Return a dict with three keys. keys and values are JAX arrays of shape (B, L, num_heads, head_dim) with the dtype of k_pages and v_pages, and mask is a bool JAX array of shape (B, L) that is True exactly at the positions j<j <j< seq_lens[b].

Examples

Input: k_pages = [[[[-1.6,-1.5]],[[1.3,-0.7]]],[[[-1.0,-2.0]],[[-0.1,0.5]]],[[[0.7,-1.2]],[[-1.5,-1.5]]],[[[-0.8,0.3]],[[-1.8,0.9]]]], v_pages = [[[[0.6,0.8]],[[-0.9,-0.4]]],[[[1.4,-0.5]],[[-0.9,0.8]]],[[[1.2,1.1]],[[-1.4,0.2]]],[[[-1.8,-0.3]],[[1.9,-0.9]]]], block_tables = [[2,0]], seq_lens = [3]

Output: {"keys":[[[[0.7,-1.2]],[[-1.5,-1.5]],[[-1.6,-1.5]],[[0.0,0.0]]]],"values":[[[[1.2,1.1]],[[-1.4,0.2]],[[0.6,0.8]],[[0.0,0.0]]]],"mask":[[true,true,true,false]]}

Explanation: With page_size 2, positions 0 and 1 form block 0, which lives in page 2, so they copy slots 0 and 1 of page 2, the keys [0.7, -1.2] and [-1.5, -1.5]. Position 2 is slot 0 of block 1, which lives in page 0, so it copies [-1.6, -1.5]. Position 3 is past seq_lens = 3, so it is zero even though slot 1 of page 0 holds [1.3, -0.7].

Input: k_pages = [[[[-1.9,0.5]],[[-0.8,0.2]]],[[[-1.7,0.9]],[[-0.8,2.0]]],[[[-0.9,-1.5]],[[-1.5,-0.7]]],[[[1.4,-0.5]],[[-1.1,-0.3]]],[[[0.1,-1.1]],[[0.7,-2.0]]],[[[0.5,0.6]],[[1.5,1.1]]]], v_pages = [[[[1.4,-0.8]],[[1.1,-1.8]]],[[[-0.8,-1.1]],[[1.4,-1.8]]],[[[-1.6,-0.5]],[[-0.2,-0.4]]],[[[-1.6,-0.2]],[[-0.4,0.5]]],[[[-1.6,-0.6]],[[1.7,-1.4]]],[[[0.1,1.9]],[[-0.5,-1.8]]]], block_tables = [[4,5,1],[3,-1,-1]], seq_lens = [5,2]

Output: {"keys":[[[[0.1,-1.1]],[[0.7,-2.0]],[[0.5,0.6]],[[1.5,1.1]],[[-1.7,0.9]],[[0.0,0.0]]],[[[1.4,-0.5]],[[-1.1,-0.3]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]]],"values":[[[[-1.6,-0.6]],[[1.7,-1.4]],[[0.1,1.9]],[[-0.5,-1.8]],[[-0.8,-1.1]],[[0.0,0.0]]],[[[-1.6,-0.2]],[[-0.4,0.5]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]]],"mask":[[true,true,true,true,true,false],[true,true,false,false,false,false]]}

Input: k_pages = [[[[1.9,1.6]],[[-0.9,-1.3]],[[-2.0,-1.0]]],[[[-1.8,-1.5]],[[-0.1,0.6]],[[-1.6,0.2]]],[[[-0.7,1.7]],[[0.5,-0.7]],[[1.2,-1.9]]]], v_pages = [[[[0.9,-1.6]],[[1.4,1.4]],[[-1.7,-1.3]]],[[[-0.8,-1.5]],[[-1.1,1.6]],[[-0.5,1.2]]],[[[1.5,-1.1]],[[-0.2,0.3]],[[-1.7,0.9]]]], block_tables = [[-1],[1]], seq_lens = [0,3]

Output: {"keys":[[[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]],[[[-1.8,-1.5]],[[-0.1,0.6]],[[-1.6,0.2]]]],"values":[[[[0.0,0.0]],[[0.0,0.0]],[[0.0,0.0]]],[[[-0.8,-1.5]],[[-1.1,1.6]],[[-0.5,1.2]]]],"mask":[[false,false,false],[true,true,true]]}

Hint 1

pos = jnp.arange(L); pos // page_size picks the block_tables column and pos % page_size the slot.

Hint 2

k_pages[pages, slots] with pages of shape (B, L) and slots of shape (L,) gathers every position at once.

Hint 3

jnp.where(mask[:, :, None, None], gathered, 0) zeroes padding and keeps the dtype.

Requirements

  • The function is compiled with jax.jit and every argument is traced, so the sequence lengths and page indices are known only as traced arrays. Python loops or if statements that depend on their values fail.

Constraints

  • num_pages is between 3 and 12, page_size is between 1 and 4, num_heads is between 1 and 2, and head_dim is between 1 and 3.
  • B is between 1 and 4 and max_blocks is between 1 and 3.
  • k_pages and v_pages share one shape, are float32 or bfloat16, and hold finite values.
  • block_tables is an int32 array of shape (B, max_blocks). For each sequence its first ceil(seq_lens[b] / page_size) entries are valid page indices that no other sequence uses, and its remaining entries are -1.
  • seq_lens is an int32 array of shape (B,) with values from 0 to max_blocks times page_size.
Try Similar Problems
Greedy Decode Loop with ScanMediumBeam Search StepHardSpeculative Decoding AcceptanceHardPad and Mask to Bucket LengthMediumHessian-Vector ProductMediumPer-Example Gradient ClippingMediumStraight-Through Estimator with Custom VJPMediumTraced Learning Rate ScheduleMediumExponential Moving Average with ScanMediumSpectral Norm by Power IterationMediumBatched Attention with vmapMediumPairwise Distances with Nested vmapMedium

Accepts: array

Accepts: array

Accepts: array

Accepts: array

You must run your code first.
Problems
Loading...
1 / 1