spark_prefill_dense.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """TP4/TP8 query-row partition (rhys101 SG18, topology check relaxed to 4 or 8 ranks) for the traced native SM121 FP4 prefill indexer.
  2. The native scorer, ragged top-k and FP4 quantization remain the serving ones.
  3. Source masks travel as lossless block IDs; no floating-point collective is used.
  4. """
  5. from __future__ import annotations
  6. from dataclasses import dataclass
  7. import logging
  8. import os
  9. import torch
  10. import torch.nn.functional as F
  11. logger=logging.getLogger(__name__)
  12. def partition(rows,world,rank,tile=128):
  13. if rows<0 or world<1 or not 0<=rank<world or tile<1:
  14. raise ValueError((rows,world,rank,tile))
  15. extent=((rows+world*tile-1)//(world*tile))*tile
  16. start=rank*extent
  17. return start,min(rows,start+extent),extent
  18. def candidate_block_ids(logits,lens,topk_blocks,block_size):
  19. scores=F.pad(logits,(0,-logits.shape[-1]%block_size),value=-torch.inf)
  20. scores=scores.unflatten(-1,(-1,block_size)).amax(-1)
  21. scores=scores.masked_fill(torch.arange(scores.shape[-1],device=logits.device)==(lens-1)//block_size,torch.inf)
  22. top=scores.topk(min(topk_blocks,scores.shape[-1]),dim=-1)
  23. return top.indices.masked_fill(~(top.values>-torch.inf),-1).to(torch.int32)
  24. def candidate_mask(ids,width,block_size):
  25. blocks=(width+block_size-1)//block_size
  26. cols=ids.long().masked_fill(ids<0,blocks)
  27. keep=torch.zeros((ids.shape[0],blocks+1),dtype=torch.bool,device=ids.device).scatter_(1,cols,True)
  28. return keep[:,:blocks].repeat_interleave(block_size,dim=-1)[:,:width]
  29. def local_selections(indexer,q_fp4,q_sf,k_fp4,k_sf,weights,ks,lens,
  30. lengths,q_lengths,world,rank,scorer,topk,mask_topk,consume):
  31. rows=q_fp4.shape[0]
  32. start,end,extent=partition(rows,world,rank)
  33. source=indexer.is_candidate_source
  34. blocks=min(indexer.candidate_topk_blocks,(max(lengths)+indexer.candidate_block_size-1)//indexer.candidate_block_size) if source else 0
  35. packed=torch.full((extent,indexer.index_topk+blocks),-1,dtype=torch.int32,device=q_fp4.device)
  36. if start>=end:
  37. return packed
  38. local=slice(start,end)
  39. local_lens=lens[local];local_ks=ks[local]
  40. logits=scorer((q_fp4[local],q_sf[local]),(k_fp4,k_sf),weights[local],local_ks,local_ks+local_lens,((max(lengths)+3)//4)*4)
  41. offset=0
  42. for request,(width,count) in enumerate(zip(lengths,q_lengths)):
  43. lo=max(start,offset);hi=min(end,offset+count)
  44. if width and lo<hi:
  45. take=slice(lo-start,hi-start);scores=logits[take,:width]
  46. if source:
  47. row_lens=local_lens[take,None]
  48. scores.masked_fill_(torch.arange(width,device=logits.device)[None,:]>=row_lens,-torch.inf)
  49. # Preserve the baseline's copy bound inside the local partition.
  50. step=max(1,(1<<30)//(width*4))
  51. nblocks=min(indexer.candidate_topk_blocks,(width+indexer.candidate_block_size-1)//indexer.candidate_block_size)
  52. for row in range(0,hi-lo,step):
  53. ids=candidate_block_ids(scores[row:row+step],row_lens[row:row+step],indexer.candidate_topk_blocks,indexer.candidate_block_size)
  54. packed[lo-start+row:lo-start+row+ids.shape[0],indexer.index_topk:indexer.index_topk+nblocks]=ids
  55. elif indexer.uses_candidates:
  56. scores.masked_fill_(~consume[request][lo-offset:hi-offset],-torch.inf)
  57. offset+=count
  58. assert offset==rows
  59. selected=torch.empty((end-start,indexer.index_topk),dtype=torch.int32,device=logits.device)
  60. topk(logits,local_lens,out_offsets=local_ks,out_indices=selected)
  61. if indexer.uses_candidates and not source:
  62. selected=mask_topk(logits,selected,local_ks)
  63. packed[:end-start,:indexer.index_topk]=selected
  64. return packed
  65. @dataclass
  66. class NativePrefillTPSplit:
  67. group:object
  68. rank:int
  69. world:int
  70. enabled:bool
  71. minimum_context:int=32768
  72. minimum_rows:int=1024
  73. def __post_init__(self):self._logged=set()
  74. @classmethod
  75. def from_parallel(cls,parallel,*,is_draft):
  76. group=parallel.attn_tp_group
  77. settings=(os.environ.get('SPARK_PREFILL_TP_SPLIT','0'),os.environ.get('SPARK_PREFILL_TP_MIN_CONTEXT','32768'),os.environ.get('SPARK_PREFILL_TP_MIN_ROWS','1024'))
  78. agreed=group.all_gather_object(settings)
  79. if any(x!=settings for x in agreed):raise RuntimeError(f'Inconsistent native prefill TP settings: {agreed}')
  80. if settings[0] not in ('0','1'):raise ValueError(settings)
  81. context,rows=map(int,settings[1:])
  82. if context<32768 or rows<1024:raise ValueError(settings)
  83. topology=(group.world_size in (4,8) and parallel.attn_cp_size==parallel.attn_dp_size==parallel.attn_dcp_size==1)
  84. hardware=torch.cuda.is_available() and torch.cuda.get_device_capability()==(12,1)
  85. enabled=settings[0]=='1' and topology and hardware and not is_draft
  86. logger.info('SPARK_NATIVE_PREFILL_TP_INIT rank=%s enabled=%s context=%s rows=%s draft=%s',group.rank_in_group,enabled,context,rows,is_draft)
  87. return cls(group,group.rank_in_group,group.world_size,enabled,context,rows)
  88. def eligible(self,rows,width,*,prefill,device):
  89. return self.enabled and prefill and device.type=='cuda' and rows>=self.minimum_rows and width>=self.minimum_context and not torch.cuda.is_current_stream_capturing()
  90. def select(self,indexer,q_fp4,q_sf,k_fp4,k_sf,weights,ks,lens,lengths,q_lengths,
  91. *,scorer,topk,mask_topk,consume,layer_id,ratio):
  92. local=local_selections(indexer,q_fp4,q_sf,k_fp4,k_sf,weights,ks,lens,lengths,q_lengths,
  93. self.world,self.rank,scorer,topk,mask_topk,consume)
  94. gathered=self.group.all_gather(local,dim=0)[:q_fp4.shape[0]]
  95. masks=None
  96. if indexer.is_candidate_source:
  97. masks=[];offset=0
  98. for width,count in zip(lengths,q_lengths):
  99. if not width or not count:masks.append(torch.zeros((0,0),dtype=torch.bool,device=gathered.device))
  100. else:
  101. nblocks=min(indexer.candidate_topk_blocks,(width+indexer.candidate_block_size-1)//indexer.candidate_block_size)
  102. ids=gathered[offset:offset+count,indexer.index_topk:indexer.index_topk+nblocks]
  103. masks.append(candidate_mask(ids,width,indexer.candidate_block_size))
  104. offset+=count
  105. key=(ratio,bool(indexer.is_candidate_source),bool(indexer.uses_candidates))
  106. if key not in self._logged:
  107. self._logged.add(key);start,end,extent=partition(q_fp4.shape[0],self.world,self.rank)
  108. logger.info('SPARK_NATIVE_PREFILL_TP_DISPATCH rank=%s layer=%s ratio=%s source=%s consumer=%s rows=%s width=%s start=%s end=%s gather_shape=%s',self.rank,layer_id,ratio,indexer.is_candidate_source,indexer.uses_candidates,q_fp4.shape[0],max(lengths),start,end,tuple(local.shape))
  109. return gathered[:,:indexer.index_topk],masks