prefill_empty_cache.py 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. """Give a long prefill's transient memory back after every chunk.
  2. Each 2048-token chunk of a DeepSeek-V4 prefill scores its queries against the whole
  3. prefix so far: the indexer logits are a [chunk, prefix] fp32 buffer per low-ratio
  4. source layer (deepseek_v4_backend: _dense_fp4_mqa_logits with max_seqlen_k = the
  5. longest compressed length), plus candidate masks and top-k scratch of the same shape.
  6. They are freed after the layer, but PyTorch's native caching allocator cannot serve
  7. the next chunk's slightly larger request from the smaller cached block, so it
  8. cudaMallocs a new one every chunk and reserved memory grows with the SQUARE of the
  9. prompt (logs/profile-2026-09-10/REPORT.md sections 15 and 17). On a Spark that
  10. reserved memory is host memory: a 34k-token prompt kept 1-1.5 GB, 64k exhausted the
  11. head. expandable_segments would coalesce it but produces NaN logits on this stack.
  12. This hook calls torch.cuda.empty_cache() after every extend forward whose longest
  13. sequence is at least DSV41_PREFILL_EMPTY_CACHE_TOKENS (default 8192): the unused
  14. cached blocks go back to the driver, the next chunk allocates fresh, and the peak is
  15. one chunk's live set instead of the sum over chunks. Decode CUDA graphs live in
  16. private pools and are untouched. Cost: one device sync and a few cudaMalloc/cudaFree
  17. per long-prefill chunk (milliseconds against a 300-800 ms chunk). 0 disables it.
  18. """
  19. import logging
  20. import os
  21. import torch
  22. logger = logging.getLogger(__name__)
  23. def install(module):
  24. threshold = int(os.environ.get('DSV41_PREFILL_EMPTY_CACHE_TOKENS', '8192'))
  25. if threshold <= 0:
  26. return
  27. cls = module.ModelRunner
  28. original = cls.forward
  29. def forward(self, forward_batch, *args, **kwargs):
  30. output = original(self, forward_batch, *args, **kwargs)
  31. try:
  32. if forward_batch.forward_mode.is_extend_without_speculative():
  33. lens = forward_batch.seq_lens_cpu
  34. longest = int(lens.max()) if lens is not None and lens.numel() else 0
  35. if longest >= threshold:
  36. torch.cuda.empty_cache()
  37. except Exception as exc: # bookkeeping must never break a forward
  38. logger.warning('DSV41 prefill empty_cache hook: %s', exc)
  39. return output
  40. cls.forward = forward
  41. logger.warning('DSV41 prefill empty_cache hook installed (extend forwards with a '
  42. 'sequence >= %d tokens)', threshold)