-
Notifications
You must be signed in to change notification settings - Fork 275
feat: add topology-aware sparse MLA candidate policy #1162
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
Draft
teerthsharma
wants to merge
37
commits into
alibaba:main
Choose a base branch
from
teerthsharma:feat/topology-inference-sparisty
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
a50d1c0
bench: add topology kv candidate schedule benchmark
teerthsharma 1a4995d
bench: document topology kv wsl validation
teerthsharma aeb4a9c
bench: use topology candidates in sparse benchmark
teerthsharma 376adbc
bench: address topology benchmark review comments
teerthsharma b449552
bench: address remaining copilot feedback
teerthsharma 9ff763e
bench: keep local tokens for partial candidate budgets
teerthsharma a051415
bench: address copilot cuda benchmark feedback
teerthsharma 2d11977
bench: address topology benchmark review cleanup
teerthsharma 21b2b7c
bench: address remaining topology review comments
teerthsharma 322fe13
bench: preserve query block in candidate rows
teerthsharma 9bc5e5b
bench: harden topology candidate scheduler
teerthsharma fc05343
bench: resolve topology benchmark copilot nits
teerthsharma 228d60c
bench: avoid direct cuda schedule tensor construction
teerthsharma a1fa068
bench: keep topology benchmark out of wildcard gpu builds
teerthsharma 5e61175
feat: add topology kv compression policy
teerthsharma b89d011
feat: gate topology kv sparse indexer policy
teerthsharma 71b46fb
test: add cuda e2e topology kv policy coverage
teerthsharma 67fe92a
chore: keep topology inference pr code only
teerthsharma 629314e
fix: harden topology kv coordinate contract
teerthsharma ec6e5f1
fix: address topology kv review feedback
teerthsharma 5b6df84
fix: eliminate topology kv review nits
teerthsharma fbc0fa9
fix: remove topology kv review blockers
teerthsharma 11d9081
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma c3478ce
fix: declare topology kv test runfiles
teerthsharma a001aeb
fix: tighten topology kv policy contracts
teerthsharma 4527e01
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma fbebed9
fix: harden topology kv policy guards
teerthsharma b1798c0
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma 70d1344
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma 3fe5ab7
fix: tighten topology only and dtype guards
teerthsharma 40c02bb
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma 1e8d413
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma 6999dec
fix: reject empty topology-only candidates
teerthsharma 5715142
fix: make topology prefill policy observable
teerthsharma 1570982
fix: gate and report topology fallbacks
teerthsharma 98d3ccf
fix: enforce topology production test contracts
teerthsharma 619f99a
Merge branch 'main' into feat/topology-inference-sparisty
teerthsharma File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| load("@arch_config//:arch_select.bzl", "torch_deps") | ||
|
|
||
| py_test( | ||
| name = "topology_kv_candidate_schedule_test", | ||
| srcs = [ | ||
| "test_topology_kv_candidate_schedule.py", | ||
| "topology_kv_candidate_schedule.py", | ||
| ], | ||
| main = "test_topology_kv_candidate_schedule.py", | ||
| deps = torch_deps(), | ||
| ) | ||
|
|
||
| py_test( | ||
| name = "topology_kv_policy_test", | ||
| srcs = ["test_topology_kv_policy.py"], | ||
| data = ["//rtp_llm/models_py:topology_kv_policy_runfiles"], | ||
| main = "test_topology_kv_policy.py", | ||
| deps = torch_deps() + ["//rtp_llm/metrics:metrics"], | ||
| ) | ||
|
|
||
| py_test( | ||
| name = "topology_kv_candidate_schedule_cuda_manual_test", | ||
| srcs = [ | ||
| "test_topology_kv_candidate_schedule.py", | ||
| "topology_kv_candidate_schedule.py", | ||
| ], | ||
| args = ["-k", "cuda"], | ||
| env = {"RTP_LLM_RUN_MANUAL_BENCHMARK_TESTS": "1"}, | ||
| main = "test_topology_kv_candidate_schedule.py", | ||
| tags = ["manual"], | ||
| deps = torch_deps(), | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,357 @@ | ||
| import os | ||
| import unittest | ||
| from unittest.mock import patch | ||
|
|
||
| import torch | ||
|
|
||
| try: | ||
| import benchmark.topology_kv_candidate_schedule as topology_kv_candidate_schedule | ||
| except ModuleNotFoundError as exc: | ||
| if exc.name not in {"benchmark", "benchmark.topology_kv_candidate_schedule"}: | ||
| raise | ||
| import topology_kv_candidate_schedule | ||
|
|
||
| BlockCandidateConfig = topology_kv_candidate_schedule.BlockCandidateConfig | ||
| benchmark_decode_attention = topology_kv_candidate_schedule.benchmark_decode_attention | ||
| block_schedule_to_token_indices = ( | ||
| topology_kv_candidate_schedule.block_schedule_to_token_indices | ||
| ) | ||
| build_key_block_centroids = topology_kv_candidate_schedule.build_key_block_centroids | ||
| build_block_candidate_schedule = ( | ||
| topology_kv_candidate_schedule.build_block_candidate_schedule | ||
| ) | ||
| build_topology_candidate_token_indices = ( | ||
| topology_kv_candidate_schedule.build_topology_candidate_token_indices | ||
| ) | ||
| dense_decode_attention = topology_kv_candidate_schedule.dense_decode_attention | ||
| format_benchmark_results = topology_kv_candidate_schedule.format_benchmark_results | ||
| run_decode_attention_grid = topology_kv_candidate_schedule.run_decode_attention_grid | ||
| sparse_decode_attention = topology_kv_candidate_schedule.sparse_decode_attention | ||
| _RUN_MANUAL_BENCHMARK_TESTS = ( | ||
| os.environ.get("RTP_LLM_RUN_MANUAL_BENCHMARK_TESTS") == "1" | ||
| ) | ||
|
|
||
|
|
||
| class TopologyKVCandidateScheduleTest(unittest.TestCase): | ||
| def test_schedule_keeps_sink_local_and_salient_causal_blocks(self): | ||
| centroids = torch.tensor( | ||
| [ | ||
| [1.0, 0.0], | ||
| [0.9, 0.1], | ||
| [-1.0, 0.0], | ||
| [0.0, 1.0], | ||
| [0.0, 0.9], | ||
| [0.1, 0.8], | ||
| ] | ||
| ) | ||
| config = BlockCandidateConfig( | ||
| block_size=64, | ||
| sink_blocks=1, | ||
| local_blocks=2, | ||
| salience_blocks=2, | ||
| max_candidate_blocks=5, | ||
| ) | ||
|
|
||
| schedule = build_block_candidate_schedule(centroids, config) | ||
|
|
||
| row = schedule[5].tolist() | ||
| self.assertEqual(row, [0, 2, 3, 4, 5]) | ||
| self.assertTrue(all(block <= 5 for block in row)) | ||
|
|
||
| def test_schedule_pads_short_rows_without_future_blocks(self): | ||
| centroids = torch.eye(3) | ||
| config = BlockCandidateConfig( | ||
| block_size=32, | ||
| sink_blocks=1, | ||
| local_blocks=2, | ||
| salience_blocks=2, | ||
| max_candidate_blocks=5, | ||
| ) | ||
|
|
||
| schedule = build_block_candidate_schedule(centroids, config) | ||
|
|
||
| self.assertEqual(schedule[0].tolist(), [0, -1, -1, -1, -1]) | ||
| self.assertEqual(schedule[1].tolist(), [0, 1, -1, -1, -1]) | ||
| self.assertEqual(schedule[2].tolist(), [0, 1, 2, -1, -1]) | ||
|
|
||
| def test_schedule_keeps_query_block_when_sink_budget_would_fill_row(self): | ||
| centroids = torch.eye(6) | ||
| config = BlockCandidateConfig( | ||
| block_size=32, | ||
| sink_blocks=5, | ||
| local_blocks=1, | ||
| salience_blocks=0, | ||
| max_candidate_blocks=5, | ||
| ) | ||
|
|
||
| schedule = build_block_candidate_schedule(centroids, config) | ||
|
|
||
| self.assertEqual(schedule[5].tolist(), [0, 1, 2, 3, 5]) | ||
|
|
||
| def test_schedule_keeps_query_block_when_local_blocks_are_disabled(self): | ||
| centroids = torch.eye(6) | ||
| config = BlockCandidateConfig( | ||
| block_size=32, | ||
| sink_blocks=5, | ||
| local_blocks=0, | ||
| salience_blocks=0, | ||
| max_candidate_blocks=5, | ||
| ) | ||
|
|
||
| schedule = build_block_candidate_schedule(centroids, config) | ||
|
|
||
| self.assertEqual(schedule[5].tolist(), [0, 1, 2, 3, 5]) | ||
|
|
||
| def test_sparse_attention_matches_dense_when_all_tokens_are_selected(self): | ||
| torch.manual_seed(0) | ||
| query = torch.randn(1, 2, 1, 16) | ||
| key = torch.randn(1, 2, 128, 16) | ||
| value = torch.randn(1, 2, 128, 16) | ||
| candidate_indices = torch.arange(128).view(1, 128) | ||
|
|
||
| dense = dense_decode_attention(query, key, value) | ||
| sparse = sparse_decode_attention(query, key, value, candidate_indices) | ||
|
|
||
| torch.testing.assert_close(sparse, dense, rtol=1e-5, atol=1e-5) | ||
|
|
||
| def test_sparse_attention_rejects_duplicate_candidate_indices(self): | ||
| query = torch.randn(1, 2, 1, 16) | ||
| key = torch.randn(1, 2, 128, 16) | ||
| value = torch.randn(1, 2, 128, 16) | ||
| candidate_indices = torch.tensor([[0, 1, 1, 2]]) | ||
|
|
||
| with self.assertRaisesRegex(ValueError, "duplicate tokens"): | ||
| sparse_decode_attention(query, key, value, candidate_indices) | ||
|
|
||
| def test_sparse_attention_rejects_ambiguous_candidate_index_shape(self): | ||
| query = torch.randn(1, 2, 1, 16) | ||
| key = torch.randn(1, 2, 128, 16) | ||
| value = torch.randn(1, 2, 128, 16) | ||
| candidate_indices = torch.tensor([[0, 1], [2, 3]]) | ||
|
|
||
| with self.assertRaisesRegex(ValueError, r"\[tokens\] or \[1, tokens\]"): | ||
| sparse_decode_attention(query, key, value, candidate_indices) | ||
|
|
||
| def test_key_block_centroids_average_batch_heads_and_tail_blocks(self): | ||
| key = torch.tensor( | ||
| [ | ||
| [ | ||
| [[1.0, 1.0], [3.0, 3.0], [10.0, 0.0], [14.0, 4.0], [5.0, 7.0]], | ||
| [[5.0, 5.0], [7.0, 7.0], [20.0, 2.0], [24.0, 6.0], [9.0, 11.0]], | ||
| ] | ||
| ] | ||
| ) | ||
|
|
||
| centroids = build_key_block_centroids(key, block_size=2) | ||
|
|
||
| expected = torch.tensor([[4.0, 4.0], [17.0, 3.0], [7.0, 9.0]]) | ||
| torch.testing.assert_close(centroids, expected) | ||
|
|
||
| def test_key_block_centroids_accept_2d_and_3d_inputs(self): | ||
| key_2d = torch.tensor( | ||
| [[1.0, 1.0], [3.0, 3.0], [10.0, 0.0], [14.0, 4.0], [5.0, 7.0]] | ||
| ) | ||
| key_3d = key_2d.unsqueeze(0) | ||
|
|
||
| expected = torch.tensor([[2.0, 2.0], [12.0, 2.0], [5.0, 7.0]]) | ||
|
|
||
| torch.testing.assert_close(build_key_block_centroids(key_2d, 2), expected) | ||
| torch.testing.assert_close(build_key_block_centroids(key_3d, 2), expected) | ||
|
|
||
| def test_key_block_centroids_reject_empty_sequence(self): | ||
| key = torch.empty(1, 1, 0, 16) | ||
|
|
||
| with self.assertRaisesRegex(ValueError, "sequence length must be positive"): | ||
| build_key_block_centroids(key, block_size=4) | ||
|
|
||
| def test_key_block_centroids_accumulate_half_precision_in_fp32(self): | ||
| key = torch.tensor( | ||
| [[1024.0], [1025.0], [1026.0], [1027.0]], | ||
| dtype=torch.float16, | ||
| ) | ||
|
|
||
| centroids = build_key_block_centroids(key, block_size=4) | ||
|
|
||
| self.assertEqual(centroids.dtype, torch.float32) | ||
| torch.testing.assert_close(centroids, torch.tensor([[1025.5]])) | ||
|
|
||
| def test_key_block_centroids_preserve_float64_accumulation(self): | ||
| key = torch.tensor( | ||
| [[1024.0], [1025.0], [1026.0], [1027.0]], | ||
| dtype=torch.float64, | ||
| ) | ||
|
|
||
| centroids = build_key_block_centroids(key, block_size=4) | ||
|
|
||
| self.assertEqual(centroids.dtype, torch.float64) | ||
| torch.testing.assert_close( | ||
| centroids, | ||
| torch.tensor([[1025.5]], dtype=torch.float64), | ||
| ) | ||
|
|
||
| def test_block_schedule_to_token_indices_expands_blocks_and_masks_tail(self): | ||
| schedule = torch.tensor([[0, 2, -1]]) | ||
|
|
||
| token_indices = block_schedule_to_token_indices( | ||
| schedule, | ||
| block_size=4, | ||
| seq_len=10, | ||
| ) | ||
|
|
||
| self.assertEqual( | ||
| token_indices.tolist(), | ||
| [[0, 1, 2, 3, 8, 9, -1, -1, -1, -1, -1, -1]], | ||
| ) | ||
|
|
||
| def test_topology_candidate_indices_use_schedule_and_keep_latest_block(self): | ||
| key = torch.arange(16, dtype=torch.float32).view(1, 1, 8, 2) | ||
|
|
||
| token_indices = build_topology_candidate_token_indices( | ||
| key, | ||
| selected_tokens=4, | ||
| block_size=2, | ||
| ) | ||
|
|
||
| self.assertEqual(token_indices.tolist(), [0, 1, 7, 6]) | ||
|
|
||
| def test_topology_candidate_indices_keep_latest_block_with_one_block_budget(self): | ||
| key = torch.arange(16, dtype=torch.float32).view(1, 1, 8, 2) | ||
|
|
||
| token_indices = build_topology_candidate_token_indices( | ||
| key, | ||
| selected_tokens=1, | ||
| block_size=2, | ||
| ) | ||
|
|
||
| self.assertEqual(token_indices.tolist(), [7]) | ||
|
|
||
| def test_topology_candidate_indices_keep_latest_tokens_for_partial_block_budget( | ||
| self, | ||
| ): | ||
| key = torch.arange(16, dtype=torch.float32).view(1, 1, 8, 2) | ||
|
|
||
| token_indices = build_topology_candidate_token_indices( | ||
| key, | ||
| selected_tokens=3, | ||
| block_size=2, | ||
| ) | ||
|
|
||
| self.assertEqual(token_indices.tolist(), [7, 6, 0]) | ||
|
|
||
| def test_topology_candidate_indices_return_requested_count_for_partial_final_block( | ||
| self, | ||
| ): | ||
| key = torch.arange(20, dtype=torch.float32).view(1, 1, 10, 2) | ||
|
|
||
| token_indices = build_topology_candidate_token_indices( | ||
| key, | ||
| selected_tokens=8, | ||
| block_size=4, | ||
| ) | ||
|
|
||
| self.assertEqual(len(token_indices), 8) | ||
| self.assertEqual(token_indices.tolist(), [9, 8, 7, 6, 5, 4, 0, 1]) | ||
|
|
||
| def test_topology_candidate_indices_accept_2d_and_3d_inputs(self): | ||
| key_2d = torch.arange(16, dtype=torch.float32).view(8, 2) | ||
| key_3d = key_2d.unsqueeze(0) | ||
|
|
||
| self.assertEqual( | ||
| build_topology_candidate_token_indices( | ||
| key_2d, | ||
| selected_tokens=1, | ||
| block_size=2, | ||
| ).tolist(), | ||
| [7], | ||
| ) | ||
| self.assertEqual( | ||
| build_topology_candidate_token_indices( | ||
| key_3d, | ||
| selected_tokens=1, | ||
| block_size=2, | ||
| ).tolist(), | ||
| [7], | ||
| ) | ||
|
|
||
| def test_benchmark_grid_and_markdown_format_are_reproducible(self): | ||
| results = run_decode_attention_grid( | ||
| seq_lens=[128], | ||
| selected_tokens=[32, 64], | ||
| heads=2, | ||
| head_dim=16, | ||
| rounds=1, | ||
| warmup=0, | ||
| dtype=torch.float32, | ||
| device="cpu", | ||
| ) | ||
|
|
||
| table = format_benchmark_results(results) | ||
|
|
||
| self.assertEqual(len(results), 2) | ||
| self.assertIn("| seq_len | selected_tokens | dense_sdpa_ms |", table) | ||
| self.assertIn("| 128 | 32 |", table) | ||
| self.assertIn("| 128 | 64 |", table) | ||
|
|
||
| def test_benchmark_decode_attention_allows_zero_sparse_timing(self): | ||
| with patch.object( | ||
| topology_kv_candidate_schedule.time, | ||
| "perf_counter", | ||
| side_effect=[0.0, 0.001, 0.001, 0.001], | ||
| ): | ||
| result = benchmark_decode_attention( | ||
| seq_len=8, | ||
| selected_tokens=8, | ||
| heads=1, | ||
| head_dim=8, | ||
| rounds=1, | ||
| warmup=0, | ||
| dtype=torch.float32, | ||
| device="cpu", | ||
| ) | ||
|
|
||
| self.assertEqual(result.sparse_ms, 0.0) | ||
| self.assertEqual(result.speedup, float("inf")) | ||
|
|
||
| def test_benchmark_decode_attention_does_not_mutate_global_rng(self): | ||
| torch.manual_seed(123) | ||
| before = torch.random.get_rng_state() | ||
|
|
||
| benchmark_decode_attention( | ||
| seq_len=64, | ||
| selected_tokens=32, | ||
| heads=2, | ||
| head_dim=16, | ||
| rounds=1, | ||
| warmup=0, | ||
| dtype=torch.float32, | ||
| device="cpu", | ||
| ) | ||
|
|
||
| after = torch.random.get_rng_state() | ||
| self.assertTrue(torch.equal(before, after)) | ||
|
|
||
| @unittest.skipUnless( | ||
| _RUN_MANUAL_BENCHMARK_TESTS and torch.cuda.is_available(), | ||
| "CUDA manual benchmark tests are disabled", | ||
| ) | ||
| def test_sparse_attention_cuda_benchmark_runs_with_topology_schedule(self): | ||
| result = benchmark_decode_attention( | ||
| seq_len=16384, | ||
| selected_tokens=512, | ||
| heads=16, | ||
| head_dim=64, | ||
| rounds=60, | ||
| warmup=20, | ||
| dtype=torch.float16, | ||
| device="cuda", | ||
| ) | ||
|
|
||
| self.assertEqual(result.seq_len, 16384) | ||
| self.assertEqual(result.selected_tokens, 512) | ||
| self.assertGreater(result.dense_ms, 0.0) | ||
| self.assertGreater(result.sparse_ms, 0.0) | ||
| self.assertGreater(result.speedup, 0.0) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.