1717"""Custom Pallas flash attention kernel for TPU."""
1818
1919import functools
20- import math
2120
2221import jax
2322import jax .numpy as jnp
2423import numpy as np
2524from jax import lax
2625from jax .experimental import pallas as pl
2726from jax .experimental .pallas import tpu as pltpu
28- from jax .experimental .shard_map import shard_map
29- from jax .sharding import PartitionSpec as P
3027
3128DEFAULT_MASK_VALUE = - 0.7 * float (np .finfo (np .dtype ("float32" )).max )
3229NUM_LANES = 128
3330NUM_SUBLANES = 8
3431NT_DIM_NUMBERS = (((1 ,), (1 ,)), ((), ()))
3532
36- # Default block sizes (tuned for 720p Wan2.1 on v6e/v7x)
37- DEFAULT_BQSIZE = 3328
38- DEFAULT_BKVSIZE = 2816
39- # Cranked up to 1024 for massive MXU throughput
40- DEFAULT_BKVCOMPUTESIZE = 1024
4133# Kept at 256 to protect VPU registers (V1 Optimization)
4234DEFAULT_BKVCOMPUTEINSIZE = 256
4335
@@ -62,12 +54,10 @@ def _flash_attention_kernel(
6254 * ,
6355 mask_value : float ,
6456 grid_width : int ,
65- bq : int ,
6657 bkv : int ,
6758 bkv_compute : int ,
6859 bkv_compute_in : int ,
6960 head_dim_v : int ,
70- q_seq_len : int ,
7161 kv_seq_len : int ,
7262 use_base2_exp : bool = True ,
7363):
@@ -207,12 +197,10 @@ def _flash_attention_kernel_mhpt(
207197 * ,
208198 mask_value : float ,
209199 grid_width : int ,
210- bq : int ,
211200 bkv : int ,
212201 bkv_compute : int ,
213202 bkv_compute_in : int ,
214203 head_dim_v : int ,
215- q_seq_len : int ,
216204 kv_seq_len : int ,
217205 heads_per_tile : int ,
218206 use_base2_exp : bool = True ,
@@ -410,12 +398,10 @@ def v_index_map(h, i, j, *_):
410398 _flash_attention_kernel ,
411399 mask_value = DEFAULT_MASK_VALUE ,
412400 grid_width = grid_width ,
413- bq = bq ,
414401 bkv = bkv ,
415402 bkv_compute = bkv_compute ,
416403 bkv_compute_in = bkv_compute_in ,
417404 head_dim_v = head_dim_v ,
418- q_seq_len = actual_q_seq_len ,
419405 kv_seq_len = actual_kv_seq_len ,
420406 use_base2_exp = use_base2_exp ,
421407 ),
@@ -500,12 +486,10 @@ def out_index_map(h, i, j, *_):
500486 _flash_attention_kernel_mhpt ,
501487 mask_value = DEFAULT_MASK_VALUE ,
502488 grid_width = grid_width ,
503- bq = bq ,
504489 bkv = bkv ,
505490 bkv_compute = bkv_compute ,
506491 bkv_compute_in = bkv_compute_in ,
507492 head_dim_v = head_dim_v ,
508- q_seq_len = actual_q_seq_len ,
509493 kv_seq_len = actual_kv_seq_len ,
510494 heads_per_tile = hpt ,
511495 use_base2_exp = use_base2_exp ,
@@ -569,227 +553,3 @@ def _splash_attention(q, k, v):
569553 return _splash_attention
570554
571555
572- # ---------------------------------------------------------------------------
573- # High-level attention function with shard_map
574- # TODO: Move `tpu_custom_attention` and `make_custom_splash_sdpa`
575- # out of this file and into `attention_flax.py` where other orchestration
576- # functions live. This file should be strictly Pallas kernel definitions.
577- # ---------------------------------------------------------------------------
578-
579- def tpu_custom_attention (
580- query ,
581- key ,
582- value ,
583- mesh ,
584- * ,
585- scale = None ,
586- block_q = None ,
587- block_kv = None ,
588- block_kv_compute = None ,
589- block_kv_compute_in = None ,
590- heads_per_tile = None ,
591- use_base2_exp = True ,
592- use_experimental_scheduler = False ,
593- vmem_limit_bytes = None ,
594- flash_block_sizes = None ,
595- ):
596- _LOG2_E = 1.44269504
597- num_heads = query .shape [1 ]
598-
599- if flash_block_sizes is not None :
600- block_q = flash_block_sizes .get ("block_q" , block_q )
601- block_kv = flash_block_sizes .get ("block_kv" , block_kv )
602- block_kv_compute = flash_block_sizes .get ("block_kv_compute" , block_kv_compute )
603- block_kv_compute_in = flash_block_sizes .get ("block_kv_compute_in" , block_kv_compute_in )
604- heads_per_tile = flash_block_sizes .get ("heads_per_tile" , heads_per_tile )
605- vmem_limit_bytes = flash_block_sizes .get ("vmem_limit_bytes" , vmem_limit_bytes )
606-
607- block_q = block_q if block_q is not None else DEFAULT_BQSIZE
608- block_kv = block_kv if block_kv is not None else DEFAULT_BKVSIZE
609- block_kv_compute = block_kv_compute if block_kv_compute is not None else DEFAULT_BKVCOMPUTESIZE
610- block_kv_compute_in = block_kv_compute_in if block_kv_compute_in is not None else DEFAULT_BKVCOMPUTEINSIZE
611- heads_per_tile = heads_per_tile if heads_per_tile is not None else 1
612-
613- def _attention_on_slices (q , k , v ):
614- scale_factor = 1.0 / math .sqrt (q .shape [- 1 ]) if scale is None else scale
615- if use_base2_exp :
616- q = q * scale_factor * _LOG2_E
617- else :
618- q = q * scale_factor
619-
620- def _pad_to_multiple (x , multiple , axis ):
621- seq_len = x .shape [axis ]
622- pad_len = (multiple - seq_len % multiple ) % multiple
623- if pad_len == 0 :
624- return x , seq_len
625- pad_width = [(0 , 0 )] * x .ndim
626- pad_width [axis ] = (0 , pad_len )
627- return jnp .pad (x , pad_width ), seq_len
628-
629- def _kernel_3d (q_3d , k_3d , v_3d ):
630- q_orig_len = q_3d .shape [1 ]
631- kv_orig_len = k_3d .shape [1 ]
632-
633- q_3d_padded , _ = _pad_to_multiple (q_3d , block_q , axis = 1 )
634- k_3d_padded , _ = _pad_to_multiple (k_3d , block_kv , axis = 1 )
635- v_3d_padded , _ = _pad_to_multiple (v_3d , block_kv , axis = 1 )
636-
637- padded_q_seq_len = q_3d_padded .shape [1 ]
638- padded_kv_seq_len = k_3d_padded .shape [1 ]
639-
640- bsizes = _BlockSizes (
641- block_q = min (block_q , padded_q_seq_len ),
642- block_kv = min (block_kv , padded_kv_seq_len ),
643- block_kv_compute = min (block_kv_compute , padded_kv_seq_len ),
644- )
645- splash_kernel = make_splash_mha (
646- block_sizes = bsizes ,
647- bkv_compute_in = block_kv_compute_in ,
648- orig_q_seq_len = q_orig_len ,
649- orig_kv_seq_len = kv_orig_len ,
650- heads_per_tile = heads_per_tile ,
651- use_base2_exp = use_base2_exp ,
652- use_experimental_scheduler = use_experimental_scheduler ,
653- vmem_limit_bytes = vmem_limit_bytes ,
654- )
655- out = splash_kernel (
656- q_3d_padded .astype (jnp .bfloat16 ),
657- k_3d_padded ,
658- v_3d_padded ,
659- )
660- out = jnp .swapaxes (out , 1 , 2 )
661- return out [:, :q_orig_len , ...]
662-
663- return jax .vmap (_kernel_3d , in_axes = (0 , 0 , 0 ), out_axes = 0 )(q , k , v )
664-
665- batch_size = query .shape [0 ]
666- if num_heads < mesh .size :
667- q_partition_spec = P ()
668- kv_partition_spec = P ()
669- out_constraint = P ()
670- else :
671- # Name-based axis resolution for the standard 4D mesh ("data", "fsdp", "context", "tensor").
672- # Axes are grouped by semantic role:
673- # - Batch-parallel: "data", "fsdp" (shard the batch dimension)
674- # - Head-parallel: "context", "tensor" (shard the head dimension)
675- # Only axes with size > 1 participate in sharding to avoid shard_map errors.
676- # Q and KV are sharded identically (symmetric head-parallel), matching the
677- # strategy that the legacy 2D (dp, tp) path used.
678- batch_axes = tuple (a for a in ("data" , "fsdp" ) if a in mesh .shape and mesh .shape [a ] > 1 )
679- head_axes = tuple (a for a in ("context" , "tensor" ) if a in mesh .shape and mesh .shape [a ] > 1 )
680-
681- batch_parallel_size = 1
682- for a in batch_axes :
683- batch_parallel_size *= mesh .shape [a ]
684- head_parallel_size = 1
685- for a in head_axes :
686- head_parallel_size *= mesh .shape [a ]
687-
688- # Flatten singleton tuples for clean PartitionSpecs.
689- batch_spec = batch_axes [0 ] if len (batch_axes ) == 1 else (batch_axes if batch_axes else None )
690- head_spec = head_axes [0 ] if len (head_axes ) == 1 else (head_axes if head_axes else None )
691-
692- if (
693- batch_parallel_size > 1
694- and batch_size >= batch_parallel_size
695- and num_heads % head_parallel_size == 0
696- ):
697- # Head-parallel: batch on data/fsdp, heads on context/tensor.
698- q_partition_spec = P (batch_spec , head_spec , None , None )
699- kv_partition_spec = P (batch_spec , head_spec , None , None )
700- out_constraint = P (batch_spec , None , head_spec , None )
701- elif head_parallel_size > 1 or batch_parallel_size > 1 :
702- # Batch too small for batch-parallel; shard all parallelism onto heads.
703- all_axes = batch_axes + head_axes
704- all_parallel_size = batch_parallel_size * head_parallel_size
705- all_spec = all_axes [0 ] if len (all_axes ) == 1 else (all_axes if all_axes else None )
706- if num_heads % all_parallel_size == 0 :
707- q_partition_spec = P (None , all_spec , None , None )
708- kv_partition_spec = P (None , all_spec , None , None )
709- out_constraint = P (None , None , all_spec , None )
710- else :
711- # Heads not divisible by available parallelism; fall back to replicated.
712- q_partition_spec = P ()
713- kv_partition_spec = P ()
714- out_constraint = P ()
715- else :
716- # All axes are size-1; fully replicated (e.g. single-device).
717- q_partition_spec = P ()
718- kv_partition_spec = P ()
719- out_constraint = P ()
720-
721- sharded_fn = shard_map (
722- _attention_on_slices ,
723- mesh = mesh ,
724- in_specs = (q_partition_spec , kv_partition_spec , kv_partition_spec ),
725- out_specs = q_partition_spec ,
726- check_rep = False ,
727- )
728- out = sharded_fn (query , key , value )
729- out = jax .lax .with_sharding_constraint (out , out_constraint )
730- return out
731-
732-
733- # ---------------------------------------------------------------------------
734- # TorchAX SDPA wrapper
735- # ---------------------------------------------------------------------------
736-
737-
738- def make_custom_splash_sdpa (mesh , env , ** kwargs ):
739- flash_block_sizes = kwargs .get ("flash_block_sizes" , None )
740- bq = kwargs .get ("block_q" , DEFAULT_BQSIZE )
741- bkv = kwargs .get ("block_kv" , DEFAULT_BKVSIZE )
742- bkv_compute = kwargs .get ("block_kv_compute" , DEFAULT_BKVCOMPUTESIZE )
743- bkv_compute_in = kwargs .get ("block_kv_compute_in" , DEFAULT_BKVCOMPUTEINSIZE )
744- hpt = kwargs .get ("heads_per_tile" , 1 )
745- use_k_smooth = kwargs .get ("use_k_smooth" , True )
746- use_base2_exp = kwargs .get ("use_base2_exp" , True )
747- use_experimental_scheduler = kwargs .get ("use_experimental_scheduler" , False )
748- vmem_limit_bytes = kwargs .get ("vmem_limit_bytes" , None )
749-
750- def _simple_attention (q , k , v , scale = None ):
751- s = scale if scale is not None else 1.0 / math .sqrt (q .shape [- 1 ])
752- attn = jnp .einsum ("bhsd,bhtd->bhst" , q * s , k )
753- attn = jax .nn .softmax (attn .astype (jnp .float32 ), axis = - 1 ).astype (q .dtype )
754- return jnp .einsum ("bhst,bhtd->bhsd" , attn , v )
755-
756- def _sdpa (
757- query ,
758- key ,
759- value ,
760- attn_mask = None ,
761- dropout_p = 0.0 ,
762- is_causal = False ,
763- scale = None ,
764- enable_gqa = False ,
765- ):
766- jquery , jkey , jvalue = env .t2j_iso ((query , key , value ))
767- num_heads = jquery .shape [1 ]
768-
769- if num_heads <= 8 :
770- result = _simple_attention (jquery , jkey , jvalue , scale = scale )
771- return env .j2t_iso (result )
772-
773- if use_k_smooth :
774- key_mean = jnp .mean (jkey , axis = 2 , keepdims = True )
775- jkey = jkey - key_mean
776-
777- result = tpu_custom_attention (
778- jquery ,
779- jkey ,
780- jvalue ,
781- mesh ,
782- scale = scale ,
783- block_q = bq ,
784- block_kv = bkv ,
785- block_kv_compute = bkv_compute ,
786- block_kv_compute_in = bkv_compute_in ,
787- heads_per_tile = hpt ,
788- use_base2_exp = use_base2_exp ,
789- use_experimental_scheduler = use_experimental_scheduler ,
790- vmem_limit_bytes = vmem_limit_bytes ,
791- flash_block_sizes = flash_block_sizes ,
792- )
793- return env .j2t_iso (result )
794-
795- return _sdpa
0 commit comments