-
Notifications
You must be signed in to change notification settings - Fork 1.2k
KV Split Oversubscription for Mixed Sequence Lengths #3379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
18ea66e
82a38d1
d612247
bb416d7
1207140
ec03b0b
531fff7
92eae0a
c5baeb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ def bench_trtllm_mla( | |
| dtype, | ||
| backend="auto", | ||
| with_sinks=False, | ||
| seq_lens_list=None, | ||
| ): | ||
| """Benchmark a single (config, backend, sinks?) cell. | ||
|
|
||
|
|
@@ -38,16 +39,20 @@ def bench_trtllm_mla( | |
| device=device, | ||
| ).to(dtype) | ||
|
|
||
| num_tokens = seq_len * batch_size | ||
| num_blocks = (num_tokens + page_size - 1) // page_size | ||
| if seq_lens_list is not None: | ||
| assert len(seq_lens_list) == batch_size, ( | ||
| f"seq_lens_list length {len(seq_lens_list)} != batch_size {batch_size}" | ||
| ) | ||
| seq_lens = list(seq_lens_list) | ||
| max_seq_len = max(seq_lens) | ||
| else: | ||
| seq_lens = [torch.randint(1, seq_len, (1,)).item() for _ in range(batch_size)] | ||
| seq_lens[-1] = seq_len | ||
| max_seq_len = max(seq_lens) | ||
|
|
||
| # Sequence lengths and block tables | ||
| seq_lens = [torch.randint(1, seq_len, (1,)).item() for _ in range(batch_size)] | ||
| seq_lens[-1] = seq_len | ||
| max_seq_len = max(seq_lens) | ||
| seq_lens_tensor = torch.tensor(seq_lens, dtype=torch.int, device=device) | ||
|
|
||
| blocks_per_seq = (seq_lens_tensor + page_size - 1) // page_size | ||
| num_blocks = int(blocks_per_seq.sum().item()) | ||
| max_num_blocks_per_seq = blocks_per_seq.max().item() | ||
|
|
||
| # Generate random but unique block IDs for all sequences | ||
|
|
@@ -80,7 +85,7 @@ def bench_trtllm_mla( | |
|
|
||
| # Allocate workspace buffer | ||
| # todo(Yingyi): calculate the actual size of workspace buffer | ||
| workspace_buffer = torch.empty(128 * 1024 * 1024, dtype=torch.int8, device=device) | ||
| workspace_buffer = torch.empty(1024 * 1024 * 1024, dtype=torch.int8, device=device) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Zero-initialize decode workspace before first kernel use. Line 88 allocates Proposed fix- workspace_buffer = torch.empty(1024 * 1024 * 1024, dtype=torch.int8, device=device)
+ workspace_buffer = torch.zeros(1024 * 1024 * 1024, dtype=torch.int8, device=device)🤖 Prompt for AI Agents |
||
|
|
||
| sinks = ( | ||
| torch.randn(num_q_heads, dtype=torch.float32, device=device) | ||
|
|
@@ -155,6 +160,16 @@ def bench_trtllm_mla( | |
| print(f"FLOPs: {flops / ms / 1e9:.2f} TFLOPs/s") | ||
|
|
||
|
|
||
| def sample_prod_distribution(batch_size: int, seed: int = 42) -> list[int]: | ||
| """Sample sequence lengths from a production-like distribution.""" | ||
| import random | ||
|
|
||
| rng = random.Random(seed) | ||
| seq_lens = [4096, 8192, 16384, 32768, 65536, 131072] | ||
| weights = [35, 20, 15, 12, 10, 8] | ||
| return rng.choices(seq_lens, weights=weights, k=batch_size) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| import argparse | ||
|
|
||
|
|
@@ -238,3 +253,27 @@ def bench_trtllm_mla( | |
| f"{type(e).__name__}: {e}" | ||
| ) | ||
| print() | ||
|
|
||
| # Mixed sequence length benchmark (production-like distribution) | ||
| for batch_size in [16, 32, 64, 128, 256]: | ||
| sl = sample_prod_distribution(batch_size, seed=batch_size) | ||
| try: | ||
| bench_trtllm_mla( | ||
| batch_size, | ||
| 1, | ||
| max(sl), | ||
| 32, | ||
| torch.bfloat16, | ||
| backend=args.backend, | ||
| seq_lens_list=sl, | ||
| ) | ||
| except ValueError as e: | ||
| print(f"SKIPPED: {e}") | ||
| print() | ||
| except Exception as e: | ||
| print( | ||
| f"ERROR: batch_size={batch_size}, q_len=1, " | ||
| f"seq_len=mixed, page_size=32, dtype=torch.bfloat16, " | ||
| f"backend={args.backend}: {type(e).__name__}: {e}" | ||
| ) | ||
| print() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -259,8 +259,7 @@ class TllmGenFmhaKernel { | |
| } | ||
|
|
||
| // Prepare the kernel parameters. | ||
| auto kernelParams = KernelParams::setKernelParams( | ||
| params, kernelMeta, ctaLaunchParams.mMaxNumCtasQ, ctaLaunchParams.mMaxNumCtasKv); | ||
| auto kernelParams = KernelParams::setKernelParams(params, kernelMeta, ctaLaunchParams); | ||
|
|
||
| // Override SageAttention parameters. | ||
| auto sageParamEncode = [](int blockSize) -> int32_t { | ||
|
|
@@ -318,8 +317,7 @@ class TllmGenFmhaKernel { | |
| "fallback to GmemReduction."); | ||
| // Rebuild kernelParams: setKernelParams uses kernelMeta (TMA descriptors, tile shapes) | ||
| // which changed when switching from CgaSmemReduction to GmemReduction kernel. | ||
| kernelParams = KernelParams::setKernelParams( | ||
| params, kernelMeta, ctaLaunchParams.mMaxNumCtasQ, ctaLaunchParams.mMaxNumCtasKv); | ||
| kernelParams = KernelParams::setKernelParams(params, kernelMeta, ctaLaunchParams); | ||
| buildLaunchConfig(launch_config, launch_attribute, kernelMeta, ctaLaunchParams, params); | ||
| setNonPortableClusterIfNeeded(func, ctaLaunchParams); | ||
| } | ||
|
|
@@ -492,10 +490,26 @@ class TllmGenFmhaKernel { | |
| // benefits of a shorter mainloop. | ||
| int const maxNumCtasPerSeqKv = | ||
| (maxAttentionWindow + 2 * kernelMeta.mStepKv - 1) / (2 * kernelMeta.mStepKv); | ||
| // Compute numCtasPerSeqKv. | ||
| numCtasPerSeqKv = std::min( | ||
| maxNumCtasPerSeqKv, | ||
| std::max(1, int32_t(params.mMultiProcessorCount / (numCtasX * numCtasY * numCtasZ)))); | ||
|
|
||
| int const baseCtas = numCtasX * numCtasY * numCtasZ; | ||
| numCtasPerSeqKv = std::min(maxNumCtasPerSeqKv, | ||
| std::max(1, int32_t(params.mMultiProcessorCount / baseCtas))); | ||
|
|
||
| // When the longest sequence needs more KV splits than the standard | ||
| // heuristic provides, oversubscribe the SMs. This helps mixed-length batches | ||
| // where long sequences get insufficient KV parallelism. | ||
| int constexpr kMinTokensPerCta = 2048; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The three constant values are too specific and might impact other cases. I would make this as an optional and user-provided options (using ENVs or something).
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added ENVs and disabled by default. |
||
| int constexpr kMaxOccupancyWaves = 16; | ||
| int constexpr kMaxSplits = 32; | ||
| int const desiredSplits = (params.mMaxSeqLenKv + kMinTokensPerCta - 1) / kMinTokensPerCta; | ||
| if (numCtasPerSeqKv < desiredSplits && desiredSplits > 4) { | ||
|
PatrykSaffer marked this conversation as resolved.
Outdated
|
||
| int const maxSplitsFromSMs = | ||
| std::max(1, int32_t(kMaxOccupancyWaves * params.mMultiProcessorCount / baseCtas)); | ||
| numCtasPerSeqKv = | ||
| std::max(numCtasPerSeqKv, | ||
| std::min({desiredSplits, maxSplitsFromSMs, maxNumCtasPerSeqKv, kMaxSplits})); | ||
| } | ||
|
|
||
| // Update the numCtasX. | ||
| numCtasX *= numCtasPerSeqKv; | ||
| // The current total number of CTAs. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.