[AMD] Optimize KIMI-K3 with Triton MLA decode kernel by tuning the stage-1 geometry for gfx950 (#34580)
Co-authored-by: Thomas Wang <thomawan@amd.com>
This commit is contained in:
co-authored by
Thomas Wang
parent
53621818e4
commit
d01812d89e
@@ -0,0 +1,259 @@
|
||||
# Copyright 2023-2026 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""The gfx950 MLA decode tuning has to stay numerically equivalent to the stock path.
|
||||
|
||||
SGLANG_MLA_DECODE_TUNE replaces the scheduler's per-sequence num_kv_splits with one
|
||||
batch-wide count. Its failure mode is silent: stage-2 derives where split `i` starts
|
||||
from the same count stage-1 used, so if only one of the two launches gets the count,
|
||||
the merge reads partials that were never written and the output is quietly wrong
|
||||
rather than an error. Only running both stages for real catches that.
|
||||
|
||||
python -m pytest test/registered/unit/layers/attention/test_mla_decode_forced_splits.py -v
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention import decode_attention as da
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.runtime_context import get_context
|
||||
from sglang.srt.utils import is_gfx95_supported, is_hip
|
||||
from sglang.test.ci.ci_register import register_amd_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
# The gfx95 runner: stage-b-test-1-gpu-large-amd is MI300, where every case below skips.
|
||||
register_amd_ci(est_time=6, suite="stage-b-test-1-gpu-small-amd-mi35x")
|
||||
|
||||
LK, LV = 576, 512
|
||||
|
||||
# A different split count reassociates the fp32 softmax reduction, so the result can
|
||||
# land a rounding step away once it is stored back to bf16. Measured at exactly one
|
||||
# bf16 ULP on every case below, hence rtol of two. The atol floor is for outputs small
|
||||
# enough that the error is set by the terms being summed rather than by the result:
|
||||
# 1.34e-3 was the most any case needed, on elements around 0.04.
|
||||
BF16_ULP = 2**-8
|
||||
ATOL = 3e-3
|
||||
|
||||
# Batches straddling every bucket edge, plus the head counts that give head_tiles 1
|
||||
# and 8, plus the sequence shapes a batch-wide split count has to survive.
|
||||
#
|
||||
# The two head_tiles=8 cases (h128) are the only ones that catch stage-1 being handed
|
||||
# the wrong count while stage-2 keeps the right one; do not drop both.
|
||||
CASES = (
|
||||
("b1_h16", [4096], 16, 1),
|
||||
("b5_h128", [4096] * 5, 128, 1),
|
||||
("b6_h16", [4096] * 6, 16, 1),
|
||||
("b24_h16", [4096] * 24, 16, 1),
|
||||
("b25_h16", [4096] * 25, 16, 1),
|
||||
("b136_h16", [1024] * 136, 16, 1),
|
||||
("mixed_lengths", [1, 31, 33, 257, 1024, 4095, 4096, 16384], 16, 1),
|
||||
("mixed_skew", [16384, 1, 1, 1, 1, 1, 1, 1], 16, 1),
|
||||
("all_length_1", [1, 1, 1, 1], 16, 1),
|
||||
("mixed_h128", [7, 512, 1023, 1025, 2048, 4095], 128, 1),
|
||||
("page64", [4096] * 4, 16, 64),
|
||||
("page64_mixed", [1 + (257 * i) % 2048 for i in range(32)], 16, 64),
|
||||
)
|
||||
|
||||
|
||||
def _inputs(seq_lens, head_num, page_size, max_kv_splits, seed):
|
||||
"""Stock decode inputs, including the num_kv_splits the scheduler would write."""
|
||||
gen = torch.Generator(device="cuda").manual_seed(seed)
|
||||
dev, batch = "cuda", len(seq_lens)
|
||||
total = sum(seq_lens)
|
||||
n_slots = total + 64
|
||||
|
||||
if page_size == 1:
|
||||
pool = torch.randn(
|
||||
n_slots, 1, LK, dtype=torch.bfloat16, device=dev, generator=gen
|
||||
)
|
||||
else:
|
||||
n_pages = (n_slots + page_size - 1) // page_size
|
||||
pool = torch.randn(
|
||||
n_pages, page_size, 1, LK, dtype=torch.bfloat16, device=dev, generator=gen
|
||||
)
|
||||
n_slots = n_pages * page_size
|
||||
|
||||
kv_indptr = torch.zeros(batch + 1, dtype=torch.int32, device=dev)
|
||||
kv_indptr[1:] = torch.cumsum(
|
||||
torch.tensor(seq_lens, dtype=torch.int32, device=dev), dim=0
|
||||
)
|
||||
# scattered slots, like a pool that has been recycled
|
||||
kv_indices = torch.randperm(n_slots, device=dev, generator=gen)[:total].to(
|
||||
torch.int32
|
||||
)
|
||||
|
||||
lens = torch.tensor(seq_lens, dtype=torch.int32, device=dev)
|
||||
num_kv_splits = torch.clamp(
|
||||
torch.div(lens, 256, rounding_mode="floor") + 1, 1, max_kv_splits
|
||||
).to(torch.int32)
|
||||
|
||||
return {
|
||||
"q": torch.randn(
|
||||
batch, head_num, LK, dtype=torch.bfloat16, device=dev, generator=gen
|
||||
),
|
||||
"k_buffer": pool,
|
||||
"v_buffer": pool[..., :LV],
|
||||
"kv_indptr": kv_indptr,
|
||||
"kv_indices": kv_indices,
|
||||
"num_kv_splits": num_kv_splits,
|
||||
"max_kv_splits": max_kv_splits,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
def _forced(inp):
|
||||
return da._mla_launch_plan(inp["q"], inp["k_buffer"], inp["max_kv_splits"], True)[1]
|
||||
|
||||
|
||||
def _run(inp):
|
||||
batch, head_num = inp["q"].shape[0], inp["q"].shape[1]
|
||||
dev, mks = "cuda", inp["max_kv_splits"]
|
||||
o = torch.zeros(batch, head_num, LV, dtype=torch.bfloat16, device=dev)
|
||||
logits = torch.empty(batch, head_num, mks, LV, dtype=torch.float32, device=dev)
|
||||
# the DCP path relies on untouched entries staying -inf
|
||||
lse = torch.full(
|
||||
(batch, head_num, mks), -float("inf"), dtype=torch.float32, device=dev
|
||||
)
|
||||
da.decode_attention_fwd_grouped(
|
||||
inp["q"],
|
||||
inp["k_buffer"],
|
||||
inp["v_buffer"],
|
||||
o,
|
||||
inp["kv_indptr"],
|
||||
inp["kv_indices"],
|
||||
logits,
|
||||
lse,
|
||||
inp["num_kv_splits"],
|
||||
mks,
|
||||
1.0 / LK**0.5,
|
||||
1.0,
|
||||
has_mla=True,
|
||||
page_size=inp["page_size"],
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
return o
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
is_hip() and is_gfx95_supported(), "the tuning only engages on gfx95"
|
||||
)
|
||||
class TestMlaDecodeForcedSplits(CustomTestCase):
|
||||
def setUp(self):
|
||||
# a plain config, so the count comes out batch-wide whatever ran before this
|
||||
self._publish()
|
||||
da._LOGGED_TUNE = False
|
||||
|
||||
def tearDown(self):
|
||||
# None, not False: the real resolution has to run again for anything later in
|
||||
# this process.
|
||||
da._KEEP_SCHEDULER_SPLITS = None
|
||||
|
||||
def _publish(self, **fields):
|
||||
override = get_context().override_server_args(**fields)
|
||||
override.install()
|
||||
self.addCleanup(override.restore)
|
||||
da._KEEP_SCHEDULER_SPLITS = None # resolve it from what was just published
|
||||
|
||||
def test_tuned_matches_stock(self):
|
||||
# Not bit-for-bit: a different split count reassociates the fp32 softmax
|
||||
# reduction. Bounded at two bf16 ULP, which is one rounding step of headroom
|
||||
# over what this actually measures.
|
||||
for seed, (name, seq_lens, head_num, page_size) in enumerate(CASES):
|
||||
with self.subTest(case=name):
|
||||
inp = _inputs(seq_lens, head_num, page_size, 256, seed=seed)
|
||||
with envs.SGLANG_MLA_DECODE_TUNE.override(False):
|
||||
stock = _run(inp)
|
||||
self.assertEqual(_forced(inp), 0)
|
||||
with envs.SGLANG_MLA_DECODE_TUNE.override(True):
|
||||
# without this the comparison passes by comparing stock to stock
|
||||
self.assertGreater(_forced(inp), 0, "tuning did not engage")
|
||||
tuned = _run(inp)
|
||||
self.assertFalse(torch.isnan(tuned).any())
|
||||
torch.testing.assert_close(
|
||||
tuned.float(),
|
||||
stock.float(),
|
||||
rtol=2 * BF16_ULP,
|
||||
atol=ATOL,
|
||||
)
|
||||
|
||||
def test_both_stages_get_the_same_count(self):
|
||||
# The mismatch the tolerance above can only catch indirectly: assert the entry
|
||||
# point hands one count to both launches instead of letting them disagree.
|
||||
# Dropping tune_mla is invisible to the numerics, it only costs the geometry.
|
||||
seen = {}
|
||||
|
||||
def record(key, real):
|
||||
def wrapper(*args, forced_kv_splits=0, **kwargs):
|
||||
seen[key] = (forced_kv_splits, kwargs.get("tune_mla"))
|
||||
return real(*args, forced_kv_splits=forced_kv_splits, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
stage1, stage2 = da._decode_grouped_att_m_fwd, da._decode_softmax_reducev_fwd
|
||||
da._decode_grouped_att_m_fwd = record("stage1", stage1)
|
||||
da._decode_softmax_reducev_fwd = record("stage2", stage2)
|
||||
try:
|
||||
with envs.SGLANG_MLA_DECODE_TUNE.override(True):
|
||||
_run(_inputs([4096] * 24, 16, 1, 256, seed=0))
|
||||
finally:
|
||||
da._decode_grouped_att_m_fwd = stage1
|
||||
da._decode_softmax_reducev_fwd = stage2
|
||||
|
||||
self.assertEqual(seen["stage1"][0], seen["stage2"][0])
|
||||
self.assertGreater(
|
||||
seen["stage1"][0], 0, "tuning did not engage, nothing tested"
|
||||
)
|
||||
self.assertTrue(seen["stage1"][1], "stage-1 was left on the stock geometry")
|
||||
|
||||
def test_scheduler_splits_are_kept_when_asked(self):
|
||||
# the geometry still changes under --enable-deterministic-inference, but it is
|
||||
# batch-independent (see the next test); only the count has to survive verbatim
|
||||
inp = _inputs([1, 4095, 4096, 16384], 16, 1, 256, seed=7)
|
||||
with envs.SGLANG_MLA_DECODE_TUNE.override(False):
|
||||
stock = _run(inp)
|
||||
self._publish(enable_deterministic_inference=True)
|
||||
with envs.SGLANG_MLA_DECODE_TUNE.override(True):
|
||||
self.assertEqual(_forced(inp), 0)
|
||||
kept = _run(inp)
|
||||
torch.testing.assert_close(
|
||||
kept.float(), stock.float(), rtol=2 * BF16_ULP, atol=ATOL
|
||||
)
|
||||
|
||||
def test_deterministic_mode_is_batch_invariant(self):
|
||||
# The point of keeping the scheduler's count: a request's reduction tree must
|
||||
# not depend on who it shares the batch with.
|
||||
self._publish(enable_deterministic_inference=True)
|
||||
lens = [4096, 1, 777, 16384]
|
||||
with envs.SGLANG_MLA_DECODE_TUNE.override(True):
|
||||
batched = _run(_inputs(lens, 16, 1, 256, seed=3))
|
||||
for i in range(len(lens)):
|
||||
inp = _inputs(lens, 16, 1, 256, seed=3)
|
||||
# same q and same KV slots for row i, on its own
|
||||
start, end = inp["kv_indptr"][i].item(), inp["kv_indptr"][i + 1].item()
|
||||
alone = dict(inp)
|
||||
alone["q"] = inp["q"][i : i + 1].clone()
|
||||
alone["kv_indices"] = inp["kv_indices"][start:end].clone()
|
||||
alone["kv_indptr"] = torch.tensor(
|
||||
[0, end - start], dtype=torch.int32, device="cuda"
|
||||
)
|
||||
alone["num_kv_splits"] = inp["num_kv_splits"][i : i + 1].clone()
|
||||
with self.subTest(row=i, seq_len=lens[i]):
|
||||
torch.testing.assert_close(
|
||||
_run(alone)[0], batched[i], rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,224 @@
|
||||
# Copyright 2023-2026 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Stage-1 split budget for the gfx950 MLA decode geometry.
|
||||
|
||||
Stage-1 launches `batch * head_tiles * kv_splits` workgroups, and crossing the
|
||||
bucket's budget costs a step rather than a proportional slice: at batch 24 on a 68k
|
||||
context, 21 splits measured 358 us against 528 us for 22. Which is also why the
|
||||
budget is divided down with floor and not round -- `round(512/12) = 43` would put
|
||||
batch 12 at 516 blocks, just over.
|
||||
|
||||
The budget lives with the geometry rather than as a global constant, since halving
|
||||
`num_warps` moved the cliff from 512 blocks to 1024. These tests pin the two
|
||||
together, so retuning one without the other, or rounding the division up, fails here
|
||||
instead of costing 50% at one batch size.
|
||||
|
||||
python -m pytest test/registered/unit/layers/attention/test_mla_decode_geometry.py -v
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from sglang.kernels.ops.attention import decode_attention as da
|
||||
from sglang.kernels.ops.attention.decode_attention import (
|
||||
_MLA_BLOCK_N,
|
||||
_MLA_BUCKET_BATCH_FREE,
|
||||
_MLA_BUCKETS,
|
||||
_fwd_grouped_kernel_stage1,
|
||||
_grouped_head_tiles,
|
||||
_keep_scheduler_splits,
|
||||
_mla_bucket,
|
||||
_mla_kv_splits,
|
||||
_mla_split_budget,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.runtime_context import get_context
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
|
||||
|
||||
# gfx950 full-GPU. Passed in rather than read from the device so this stays a CPU test.
|
||||
CORE_COUNT = 256
|
||||
|
||||
# The batch sizes cuda-graph capture walks, and the split count separately
|
||||
# measured as optimal at each on a 68k context (head_tiles == 1, i.e. K3 at tp 8).
|
||||
MEASURED_OPTIMUM = {
|
||||
1: 112,
|
||||
2: 112,
|
||||
3: 85,
|
||||
4: 64,
|
||||
5: 51,
|
||||
6: 85,
|
||||
7: 73,
|
||||
8: 64,
|
||||
10: 51,
|
||||
12: 42,
|
||||
14: 36,
|
||||
16: 32,
|
||||
20: 25,
|
||||
24: 21,
|
||||
28: 36,
|
||||
32: 32,
|
||||
}
|
||||
|
||||
MAX_KV_SPLITS = 256
|
||||
|
||||
|
||||
class TestMlaDecodeGeometry(unittest.TestCase):
|
||||
def test_rule_reproduces_measured_optimum(self):
|
||||
# a budget plus two constants, not a fit, so it hits the measured optimum
|
||||
for batch, want in MEASURED_OPTIMUM.items():
|
||||
with self.subTest(batch=batch):
|
||||
self.assertEqual(
|
||||
_mla_kv_splits(batch, 1, MAX_KV_SPLITS, CORE_COUNT), want
|
||||
)
|
||||
|
||||
def test_stage1_split_count_stays_runtime(self):
|
||||
# A constexpr here would compile one stage-1 variant per rung of the capture
|
||||
# ladder (21 for the default one), and the count only feeds kv_len_per_split.
|
||||
params = {p.name: p for p in _fwd_grouped_kernel_stage1.params}
|
||||
self.assertFalse(params["forced_kv_splits"].is_constexpr)
|
||||
self.assertTrue(params["USE_FORCED"].is_constexpr)
|
||||
|
||||
def test_batch_free_geometry_is_pinned(self):
|
||||
# Deterministic inference runs on this geometry and BLOCK_N/num_warps reorder
|
||||
# the fp32 accumulation, so retuning either moves those numbers.
|
||||
self.assertEqual(
|
||||
(
|
||||
_MLA_BLOCK_N,
|
||||
_MLA_BUCKET_BATCH_FREE.num_warps,
|
||||
_MLA_BUCKET_BATCH_FREE.num_stages,
|
||||
),
|
||||
(32, 2, 2),
|
||||
)
|
||||
|
||||
def test_head_tiles_matches_the_grid(self):
|
||||
# The budget is divided by the same head_tiles the grid is launched with; a
|
||||
# BLOCK_H that drifts between the two silently mis-sizes the budget.
|
||||
for head_num, kv_group_num, want in ((16, 16, 1), (128, 128, 8), (8, 8, 1)):
|
||||
with self.subTest(head_num=head_num):
|
||||
self.assertEqual(_grouped_head_tiles(head_num, kv_group_num), want)
|
||||
|
||||
def test_never_crosses_the_budget(self):
|
||||
for head_tiles in (1, 2):
|
||||
for batch in range(1, 1025):
|
||||
splits = _mla_kv_splits(batch, head_tiles, MAX_KV_SPLITS, CORE_COUNT)
|
||||
budget = _mla_split_budget(_mla_bucket(batch).num_warps, CORE_COUNT)
|
||||
with self.subTest(batch=batch, head_tiles=head_tiles):
|
||||
if batch * head_tiles <= budget:
|
||||
self.assertLessEqual(batch * head_tiles * splits, budget)
|
||||
else:
|
||||
# already past the budget, so 1 is the floor
|
||||
self.assertEqual(splits, 1)
|
||||
|
||||
def test_low_batch_is_capped_not_scaled(self):
|
||||
# below 6 the budget stops binding, and more splits stopped paying at 112
|
||||
# whatever the batch, so the cap sits on top of the budget instead of scaling
|
||||
self.assertEqual(_mla_kv_splits(1, 1, MAX_KV_SPLITS, CORE_COUNT), 112)
|
||||
self.assertEqual(_mla_kv_splits(2, 1, MAX_KV_SPLITS, CORE_COUNT), 112)
|
||||
self.assertLess(
|
||||
1 * 112, _mla_split_budget(_mla_bucket(1).num_warps, CORE_COUNT)
|
||||
)
|
||||
|
||||
def test_caller_max_kv_splits_wins(self):
|
||||
for cap in (1, 4, 16):
|
||||
with self.subTest(max_kv_splits=cap):
|
||||
self.assertLessEqual(_mla_kv_splits(1, 1, cap, CORE_COUNT), cap)
|
||||
|
||||
def test_at_least_one_split(self):
|
||||
for batch in (1, 4096):
|
||||
with self.subTest(batch=batch):
|
||||
self.assertGreaterEqual(
|
||||
_mla_kv_splits(batch, 1, MAX_KV_SPLITS, CORE_COUNT), 1
|
||||
)
|
||||
|
||||
def test_buckets_are_ordered_and_total(self):
|
||||
bounds = [b.batch_max for b in _MLA_BUCKETS]
|
||||
self.assertIsNone(bounds[-1], "the last bucket has to be the catch-all")
|
||||
finite = bounds[:-1]
|
||||
self.assertEqual(finite, sorted(finite))
|
||||
self.assertTrue(all(b is not None for b in finite))
|
||||
|
||||
def test_wider_workgroup_gets_a_tighter_budget(self):
|
||||
# the budget divides by num_warps because that is where the cliff moved:
|
||||
# halving the warps took it from 512 blocks to 1024
|
||||
budgets = [
|
||||
_mla_split_budget(w, CORE_COUNT)
|
||||
for w in sorted({b.num_warps for b in _MLA_BUCKETS}, reverse=True)
|
||||
]
|
||||
self.assertEqual(budgets, sorted(budgets))
|
||||
|
||||
def test_budget_follows_the_partition_size(self):
|
||||
# A CPX partition exposes 32 of the 256 CUs while is_gfx95_supported() still
|
||||
# says yes, so a budget pinned to the whole GPU would oversubscribe it 8x.
|
||||
for warps in (1, 2, 4):
|
||||
with self.subTest(num_warps=warps):
|
||||
self.assertEqual(
|
||||
_mla_split_budget(warps, 32) * 8,
|
||||
_mla_split_budget(warps, 256),
|
||||
)
|
||||
self.assertEqual(
|
||||
_mla_kv_splits(8, 1, MAX_KV_SPLITS, 0), 0, "no core count, no budget"
|
||||
)
|
||||
|
||||
def test_splits_shrink_on_a_partition(self):
|
||||
# Same batch, smaller partition -> fewer splits, never more.
|
||||
for batch in (8, 16, 32, 64):
|
||||
with self.subTest(batch=batch):
|
||||
self.assertLessEqual(
|
||||
_mla_kv_splits(batch, 1, MAX_KV_SPLITS, 32),
|
||||
_mla_kv_splits(batch, 1, MAX_KV_SPLITS, 256),
|
||||
)
|
||||
|
||||
|
||||
class TestKeepSchedulerSplits(unittest.TestCase):
|
||||
"""Which configs decline the batch-wide count.
|
||||
|
||||
Through override_server_args, so the flags resolve the way a launched server
|
||||
resolves them; poking the cached decision keeps passing after they move namespace.
|
||||
"""
|
||||
|
||||
def _publish(self, **fields):
|
||||
override = get_context().override_server_args(**fields)
|
||||
override.install()
|
||||
self.addCleanup(override.restore)
|
||||
# resolved once per process, so clear it at both ends
|
||||
da._KEEP_SCHEDULER_SPLITS = None
|
||||
self.addCleanup(setattr, da, "_KEEP_SCHEDULER_SPLITS", None)
|
||||
|
||||
def test_a_plain_config_takes_the_batch_wide_count(self):
|
||||
self._publish()
|
||||
self.assertFalse(_keep_scheduler_splits())
|
||||
|
||||
def test_deterministic_inference_keeps_the_scheduler_splits(self):
|
||||
self._publish(enable_deterministic_inference=True)
|
||||
self.assertTrue(_keep_scheduler_splits())
|
||||
|
||||
def test_an_explicit_split_tile_size_keeps_them(self):
|
||||
self._publish(triton_attention_split_tile_size=256)
|
||||
self.assertTrue(_keep_scheduler_splits())
|
||||
|
||||
def test_the_static_kv_splits_env_keeps_them(self):
|
||||
self._publish()
|
||||
with envs.SGLANG_TRITON_DECODE_ATTN_STATIC_KV_SPLITS.override(True):
|
||||
self.assertTrue(_keep_scheduler_splits())
|
||||
|
||||
def test_the_geometry_rule_does_not_read_the_config(self):
|
||||
# _mla_kv_splits answers for a device, not for a config; the decline lives one
|
||||
# level up, so a deterministic config elsewhere cannot rewrite the pins above
|
||||
self._publish(enable_deterministic_inference=True)
|
||||
self.assertEqual(_mla_kv_splits(24, 1, MAX_KV_SPLITS, CORE_COUNT), 21)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user