[FlashInfer V0.6.18] feat(dsv4): support --dsa-topk-backend flashinfer with fused top-k (#33237)
This commit is contained in:
@@ -1698,7 +1698,11 @@ def _set_envs_and_config(server_args: ServerArgs):
|
||||
|
||||
# Check flashinfer version
|
||||
if not get_bool_env_var("SGLANG_SKIP_SGL_KERNEL_VERSION_CHECK"):
|
||||
if "flashinfer" in attention_backends_of(resolved_view(cfg)):
|
||||
if (
|
||||
"flashinfer" in attention_backends_of(resolved_view(cfg))
|
||||
or cfg.dsa_topk_backend == "flashinfer"
|
||||
or cfg.speculative_dsa_topk_backend == "flashinfer"
|
||||
):
|
||||
assert_pkg_version(
|
||||
"flashinfer_python",
|
||||
"0.6.18",
|
||||
|
||||
@@ -704,6 +704,7 @@ class DeepseekV4AttnBackend(
|
||||
page_size=self.page_size,
|
||||
page_table=core_attn_metadata.page_table,
|
||||
c4_seq_lens=core_attn_metadata.c4_topk_lengths_raw,
|
||||
use_topk_v2=self.dsa_topk_backend.should_use_topk_v2() and not _is_xpu,
|
||||
# The SM120 FP4 kernel schedules split_kv=128, while the generic
|
||||
# JIT metadata planner encodes split_kv=256.
|
||||
force_deep_gemm_metadata=(
|
||||
|
||||
@@ -492,6 +492,7 @@ class DeepseekV4HipRadixBackend(
|
||||
page_size=self.page_size,
|
||||
page_table=core_attn_metadata.page_table,
|
||||
c4_seq_lens=core_attn_metadata.c4_topk_lengths_raw,
|
||||
use_topk_v2=False,
|
||||
)
|
||||
|
||||
def init_forward_metadata_decode(
|
||||
|
||||
@@ -404,11 +404,44 @@ def topk_transform_512_flashinfer_unfused(
|
||||
)
|
||||
|
||||
|
||||
def topk_transform_512_flashinfer_fused(
|
||||
scores: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
page_tables: torch.Tensor,
|
||||
out_page_indices: torch.Tensor,
|
||||
page_size: int,
|
||||
out_raw_indices: Optional[torch.Tensor] = None,
|
||||
) -> None:
|
||||
import flashinfer
|
||||
|
||||
from sglang.srt.layers.attention.dsa.dsa_topk_backend import (
|
||||
_flashinfer_tie_break_value,
|
||||
)
|
||||
|
||||
flashinfer.top_k_page_table_transform(
|
||||
scores,
|
||||
page_tables.contiguous(),
|
||||
seq_lens.contiguous(),
|
||||
out_page_indices.shape[1],
|
||||
deterministic=envs.SGLANG_DSA_TOPK_FLASHINFER_DETERMINISTIC.get(),
|
||||
tie_break=_flashinfer_tie_break_value(),
|
||||
dsa_graph_safe=True,
|
||||
page_size=page_size,
|
||||
out=out_page_indices,
|
||||
out_raw_indices=out_raw_indices,
|
||||
)
|
||||
|
||||
|
||||
class C4IndexerBackendMixin:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.debug_use_external_c4_sparse_indices: bool = False
|
||||
self.dsa_topk_backend: DSATopKBackend = DSATopKBackend.SGL_KERNEL
|
||||
self.flashinfer_topk_transform: Callable[..., None] = (
|
||||
topk_transform_512_flashinfer_fused
|
||||
if envs.SGLANG_DSA_FUSE_TOPK.get()
|
||||
else topk_transform_512_flashinfer_unfused
|
||||
)
|
||||
|
||||
def _forward_prepare_multi_stream(
|
||||
self,
|
||||
@@ -829,7 +862,7 @@ class C4IndexerBackendMixin:
|
||||
raw_indices,
|
||||
)
|
||||
elif self.dsa_topk_backend.is_flashinfer():
|
||||
topk_transform_512_flashinfer_unfused(
|
||||
self.flashinfer_topk_transform(
|
||||
logits,
|
||||
c4_seq_lens,
|
||||
page_table,
|
||||
@@ -837,7 +870,7 @@ class C4IndexerBackendMixin:
|
||||
indexer_metadata.c4_page_size,
|
||||
raw_indices,
|
||||
)
|
||||
elif envs.SGLANG_OPT_USE_TOPK_V2.get() and raw_indices is None:
|
||||
elif self.dsa_topk_backend.should_use_topk_v2() and raw_indices is None:
|
||||
topk_transform_512_v2(
|
||||
logits,
|
||||
c4_seq_lens,
|
||||
|
||||
@@ -112,6 +112,7 @@ class PagedIndexerMetadata:
|
||||
page_size: int
|
||||
page_table: torch.Tensor
|
||||
c4_seq_lens: torch.Tensor
|
||||
use_topk_v2: bool
|
||||
force_deep_gemm_metadata: bool = False
|
||||
use_prefill_cuda_graph: bool = False
|
||||
deep_gemm_metadata: Any = field(init=False, repr=False)
|
||||
@@ -152,7 +153,7 @@ class PagedIndexerMetadata:
|
||||
|
||||
assert isinstance(self.deep_gemm_metadata, torch.Tensor)
|
||||
|
||||
if envs.SGLANG_OPT_USE_TOPK_V2.get() and not is_xpu():
|
||||
if self.use_topk_v2:
|
||||
from sglang.kernels.ops.attention.dsv4 import plan_topk_v2
|
||||
|
||||
self.topk_metadata = plan_topk_v2(self.c4_seq_lens)
|
||||
@@ -188,6 +189,7 @@ class PagedIndexerMetadata:
|
||||
"page_size",
|
||||
"force_deep_gemm_metadata",
|
||||
"use_prefill_cuda_graph",
|
||||
"use_topk_v2",
|
||||
],
|
||||
copy_fields=copy_fields,
|
||||
assign_fields=assign_fields,
|
||||
|
||||
@@ -19,11 +19,11 @@ PAGE_SIZE = 64
|
||||
DISABLE_TORCH = True
|
||||
|
||||
|
||||
def _make_inputs(batch_size: int, seq_len: int, k: int):
|
||||
def _make_inputs(batch_size: int, seq_len: int, k: int, page_size: int = PAGE_SIZE):
|
||||
torch.random.manual_seed(42)
|
||||
scores = torch.randn(batch_size, seq_len, dtype=torch.float32, device="cuda")
|
||||
seq_lens = torch.full((batch_size,), seq_len, dtype=torch.int32, device="cuda")
|
||||
num_pages = (seq_len + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
num_pages = (seq_len + page_size - 1) // page_size
|
||||
page_table = (
|
||||
torch.arange(num_pages, dtype=torch.int32, device="cuda")
|
||||
.unsqueeze(0)
|
||||
@@ -34,22 +34,11 @@ def _make_inputs(batch_size: int, seq_len: int, k: int):
|
||||
return scores, seq_lens, page_table, out
|
||||
|
||||
|
||||
def _make_p1_table(batch_size: int, seq_len: int):
|
||||
# flashinfer / torch do a per-token (page_size=1) gather, so they need a
|
||||
# (batch, seq) table (one entry per position) rather than the page-size-64 one.
|
||||
src_page_table = (
|
||||
torch.arange(seq_len, dtype=torch.int32, device="cuda")
|
||||
.unsqueeze(0)
|
||||
.expand(batch_size, -1)
|
||||
.contiguous()
|
||||
)
|
||||
lengths = torch.full((batch_size,), seq_len, dtype=torch.int32, device="cuda")
|
||||
return src_page_table, lengths
|
||||
|
||||
|
||||
def _build_paged_fn(provider: str, batch_size: int, seq_len: int, k: int):
|
||||
scores, seq_lens, page_table, out = _make_inputs(batch_size, seq_len, k)
|
||||
N = PAGE_SIZE
|
||||
def _build_paged_fn(
|
||||
provider: str, batch_size: int, seq_len: int, k: int, page_size: int
|
||||
):
|
||||
scores, seq_lens, page_table, out = _make_inputs(batch_size, seq_len, k, page_size)
|
||||
N = page_size
|
||||
|
||||
def fn(scores, seq_lens, page_table):
|
||||
if provider == "jit_v1":
|
||||
@@ -61,15 +50,23 @@ def _build_paged_fn(provider: str, batch_size: int, seq_len: int, k: int):
|
||||
elif provider == "flashinfer":
|
||||
from flashinfer import top_k_page_table_transform
|
||||
|
||||
return top_k_page_table_transform(scores, page_table, seq_lens, k)
|
||||
return top_k_page_table_transform(
|
||||
scores,
|
||||
page_table,
|
||||
seq_lens,
|
||||
k,
|
||||
dsa_graph_safe=True,
|
||||
page_size=N,
|
||||
out=out,
|
||||
)
|
||||
elif provider == "torch":
|
||||
idx = scores.topk(k, dim=-1).indices # (batch, k) int64
|
||||
return torch.gather(page_table, 1, idx)
|
||||
raw_indices = scores.topk(k, dim=-1).indices
|
||||
physical_pages = torch.gather(page_table, 1, raw_indices // N)
|
||||
out.copy_(physical_pages * N + raw_indices % N)
|
||||
return out
|
||||
else:
|
||||
raise ValueError(f"unknown provider {provider}")
|
||||
|
||||
if provider in ("flashinfer", "torch"):
|
||||
page_table, seq_lens = _make_p1_table(batch_size, seq_len)
|
||||
if provider == "jit_v2":
|
||||
metadata = plan_topk_v2(seq_lens)
|
||||
return fn, (scores, seq_lens, page_table)
|
||||
@@ -110,14 +107,17 @@ if not DISABLE_TORCH:
|
||||
@marker.parametrize("k", [512, 1024, 2048], [512])
|
||||
@marker.parametrize("seq_len", [2**x for x in range(10, 19)], [4096, 65536])
|
||||
@marker.parametrize("batch_size", [2**x for x in range(13)], [1, 128, 1024])
|
||||
@marker.parametrize("page_size", [1, 64], [1, 64])
|
||||
@marker.benchmark("provider", PRROVIDERS)
|
||||
def benchmark_paged(seq_len: int, batch_size: int, k: int, provider: str):
|
||||
def benchmark_paged(
|
||||
seq_len: int, batch_size: int, k: int, page_size: int, provider: str
|
||||
):
|
||||
if k > seq_len:
|
||||
marker.skip("k cannot be larger than seq_len")
|
||||
if k == 2048 and provider == "jit_v1":
|
||||
marker.skip("jit_v1 does not support k=2048")
|
||||
|
||||
fn, input_args = _build_paged_fn(provider, batch_size, seq_len, k)
|
||||
fn, input_args = _build_paged_fn(provider, batch_size, seq_len, k, page_size)
|
||||
return marker.do_bench(fn, input_args=input_args, memory_args=input_args[:2])
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from itertools import product
|
||||
from typing import List, Optional, Tuple
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -25,6 +26,7 @@ from sglang.srt.layers.attention.dsa_backend import (
|
||||
DeepseekSparseAttnBackend,
|
||||
DSAMetadata,
|
||||
)
|
||||
from sglang.srt.layers.attention.dsv4.indexer import C4IndexerBackendMixin
|
||||
from sglang.srt.layers.layernorm import LayerNorm
|
||||
from sglang.srt.layers.linear import LinearBase
|
||||
from sglang.srt.mem_cache.memory_pool import DSATokenToKVPool
|
||||
@@ -32,7 +34,7 @@ from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMo
|
||||
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=18, stage="base-b", runner_config="1-gpu-large")
|
||||
register_cuda_ci(est_time=30, stage="base-b", runner_config="1-gpu-large")
|
||||
|
||||
# Global configuration for all indexer tests
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -450,6 +452,151 @@ class TestDSAIndexer(CustomTestCase):
|
||||
index=perm,
|
||||
)
|
||||
|
||||
def _assert_dsv4_compact_topk_result(
|
||||
self,
|
||||
scores: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
page_table: torch.Tensor,
|
||||
out_page_indices: torch.Tensor,
|
||||
out_raw_indices: Optional[torch.Tensor],
|
||||
page_size: int,
|
||||
) -> None:
|
||||
topk = out_page_indices.shape[1]
|
||||
for row in range(scores.shape[0]):
|
||||
seq_len = int(seq_lens[row].item())
|
||||
expected_count = min(seq_len, topk)
|
||||
if seq_len <= topk:
|
||||
expected_raw = torch.arange(
|
||||
seq_len, dtype=torch.int32, device=scores.device
|
||||
)
|
||||
else:
|
||||
expected_raw = torch.topk(
|
||||
scores[row, :seq_len], topk, sorted=False
|
||||
).indices.to(torch.int32)
|
||||
expected_page_indices = (
|
||||
page_table[row, expected_raw.long() // page_size] * page_size
|
||||
+ expected_raw % page_size
|
||||
)
|
||||
|
||||
actual_page_indices = out_page_indices[row]
|
||||
actual_page_prefix = actual_page_indices[:expected_count]
|
||||
self.assertTrue(
|
||||
torch.equal(
|
||||
torch.sort(actual_page_prefix).values,
|
||||
torch.sort(expected_page_indices).values,
|
||||
)
|
||||
)
|
||||
self.assertTrue(torch.all(actual_page_indices[expected_count:] == -1))
|
||||
|
||||
if out_raw_indices is not None:
|
||||
actual_raw = out_raw_indices[row]
|
||||
actual_raw_prefix = actual_raw[:expected_count]
|
||||
self.assertTrue(
|
||||
torch.equal(
|
||||
torch.sort(actual_raw_prefix).values,
|
||||
torch.sort(expected_raw).values,
|
||||
)
|
||||
)
|
||||
self.assertTrue(torch.all(actual_raw[expected_count:] == -1))
|
||||
translated_raw = (
|
||||
page_table[row, actual_raw_prefix.long() // page_size] * page_size
|
||||
+ actual_raw_prefix % page_size
|
||||
)
|
||||
self.assertTrue(torch.equal(actual_page_prefix, translated_raw))
|
||||
|
||||
def test_dsv4_flashinfer_compact_topk_cuda_graph(self):
|
||||
num_rows, max_len, page_size = 4, 2048, 64
|
||||
score_storage = torch.empty(
|
||||
(num_rows, max_len + 16), dtype=torch.float32, device=self.device
|
||||
)
|
||||
scores = score_storage[:, 1 : max_len + 1]
|
||||
self.assertFalse(scores.is_contiguous())
|
||||
|
||||
page_table = 17 + torch.arange(
|
||||
num_rows * (max_len // page_size),
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
).reshape(num_rows, max_len // page_size)
|
||||
|
||||
for fuse_topk, topk, with_raw_output in product(
|
||||
(False, True), (512, 1024), (False, True)
|
||||
):
|
||||
with self.subTest(
|
||||
fuse_topk=fuse_topk,
|
||||
topk=topk,
|
||||
with_raw_output=with_raw_output,
|
||||
):
|
||||
scores.copy_(
|
||||
torch.arange(max_len, dtype=torch.float32, device=self.device)
|
||||
.unsqueeze(0)
|
||||
.expand(num_rows, -1)
|
||||
)
|
||||
seq_lens = torch.full(
|
||||
(num_rows,), max_len, dtype=torch.int32, device=self.device
|
||||
)
|
||||
out_page_indices = torch.empty(
|
||||
(num_rows, topk), dtype=torch.int32, device=self.device
|
||||
)
|
||||
out_raw_indices = (
|
||||
torch.empty_like(out_page_indices) if with_raw_output else None
|
||||
)
|
||||
|
||||
# Capture with every output slot valid so shrinking lengths on
|
||||
# replay leaves meaningful stale values in any unwritten suffix.
|
||||
with (
|
||||
envs.SGLANG_DSA_FUSE_TOPK.override(fuse_topk),
|
||||
envs.SGLANG_DSA_TOPK_FLASHINFER_DETERMINISTIC.override(True),
|
||||
envs.SGLANG_DSA_TOPK_FLASHINFER_TIE_BREAK.override("small"),
|
||||
):
|
||||
backend = C4IndexerBackendMixin()
|
||||
backend.flashinfer_topk_transform(
|
||||
scores,
|
||||
seq_lens,
|
||||
page_table,
|
||||
out_page_indices,
|
||||
page_size,
|
||||
out_raw_indices,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
backend.flashinfer_topk_transform(
|
||||
scores,
|
||||
seq_lens,
|
||||
page_table,
|
||||
out_page_indices,
|
||||
page_size,
|
||||
out_raw_indices,
|
||||
)
|
||||
|
||||
scores.copy_(
|
||||
torch.arange(
|
||||
max_len, 0, -1, dtype=torch.float32, device=self.device
|
||||
)
|
||||
.unsqueeze(0)
|
||||
.expand(num_rows, -1)
|
||||
)
|
||||
page_table.add_(100)
|
||||
seq_lens.copy_(
|
||||
torch.tensor(
|
||||
[0, topk - 1, topk + 73, max_len],
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
)
|
||||
)
|
||||
graph.replay()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
self._assert_dsv4_compact_topk_result(
|
||||
scores,
|
||||
seq_lens,
|
||||
page_table,
|
||||
out_page_indices,
|
||||
out_raw_indices,
|
||||
page_size,
|
||||
)
|
||||
|
||||
def _run_unfused_topk_backend_validity_test(
|
||||
self,
|
||||
batch_size: int,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import sys
|
||||
import unittest
|
||||
from itertools import product
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.attention.dsv4.indexer import FP8_DTYPE, C4IndexerBackendMixin
|
||||
from sglang.srt.layers.attention.dsv4.indexer import (
|
||||
FP8_DTYPE,
|
||||
C4IndexerBackendMixin,
|
||||
)
|
||||
from sglang.srt.layers.attention.dsv4.metadata import (
|
||||
NonPagedIndexerPlan,
|
||||
PagedIndexerMetadata,
|
||||
@@ -43,6 +47,7 @@ class TestDSV4PagedIndexerMetadata(CustomTestCase):
|
||||
page_size=256,
|
||||
page_table=torch.zeros((1, 1), dtype=torch.int32),
|
||||
c4_seq_lens=torch.tensor([65], dtype=torch.int32),
|
||||
use_topk_v2=False,
|
||||
force_deep_gemm_metadata=True,
|
||||
)
|
||||
|
||||
@@ -64,10 +69,119 @@ class TestDSV4PagedIndexerMetadata(CustomTestCase):
|
||||
page_size=256,
|
||||
page_table=torch.zeros((1, 1), dtype=torch.int32),
|
||||
c4_seq_lens=torch.tensor([65], dtype=torch.int32),
|
||||
use_topk_v2=False,
|
||||
)
|
||||
|
||||
self.assertIsNone(metadata.deep_gemm_metadata)
|
||||
|
||||
def test_topk_v2_ineligible_backend_skips_plan(self):
|
||||
with (
|
||||
envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.override(True),
|
||||
envs.SGLANG_OPT_USE_AITER_INDEXER.override(False),
|
||||
envs.SGLANG_OPT_USE_TOPK_V2.override(True),
|
||||
patch("sglang.kernels.ops.attention.dsv4.plan_topk_v2") as plan_topk_v2,
|
||||
):
|
||||
metadata = PagedIndexerMetadata(
|
||||
page_size=256,
|
||||
page_table=torch.zeros((1, 1), dtype=torch.int32),
|
||||
c4_seq_lens=torch.tensor([65], dtype=torch.int32),
|
||||
use_topk_v2=False,
|
||||
)
|
||||
|
||||
plan_topk_v2.assert_not_called()
|
||||
self.assertEqual(metadata.topk_metadata.numel(), 0)
|
||||
|
||||
|
||||
class TestDSV4FlashInferTopK(CustomTestCase):
|
||||
def test_compact_page_transform_respects_fuse_topk(self):
|
||||
score_storage = torch.arange(160, dtype=torch.float32).reshape(2, 80)
|
||||
scores = score_storage[:, 1:65]
|
||||
self.assertFalse(scores.is_contiguous())
|
||||
|
||||
seq_lens = torch.tensor([63, 64], dtype=torch.int32)
|
||||
page_tables = torch.tensor(
|
||||
[[7, 17, 8, 18], [11, 21, 12, 22]], dtype=torch.int32
|
||||
)[:, ::2]
|
||||
self.assertFalse(page_tables.is_contiguous())
|
||||
out_page_indices = torch.empty((2, 8), dtype=torch.int32)
|
||||
|
||||
for fuse_topk, with_raw_output in product((False, True), repeat=2):
|
||||
with self.subTest(fuse_topk=fuse_topk, with_raw_output=with_raw_output):
|
||||
out_raw_indices = (
|
||||
torch.empty_like(out_page_indices) if with_raw_output else None
|
||||
)
|
||||
|
||||
def fake_top_k(input: torch.Tensor, k: int, **kwargs):
|
||||
return torch.topk(
|
||||
input,
|
||||
k,
|
||||
dim=-1,
|
||||
largest=True,
|
||||
sorted=kwargs["sorted"],
|
||||
)
|
||||
|
||||
top_k = MagicMock(side_effect=fake_top_k)
|
||||
top_k_page_table_transform = MagicMock()
|
||||
flashinfer = SimpleNamespace(
|
||||
top_k=top_k,
|
||||
top_k_page_table_transform=top_k_page_table_transform,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.dict(sys.modules, {"flashinfer": flashinfer}),
|
||||
envs.SGLANG_DSA_FUSE_TOPK.override(fuse_topk),
|
||||
envs.SGLANG_DSA_TOPK_FLASHINFER_DETERMINISTIC.override(True),
|
||||
envs.SGLANG_DSA_TOPK_FLASHINFER_TIE_BREAK.override("small"),
|
||||
):
|
||||
backend = C4IndexerBackendMixin()
|
||||
backend.flashinfer_topk_transform(
|
||||
scores,
|
||||
seq_lens,
|
||||
page_tables,
|
||||
out_page_indices,
|
||||
page_size=64,
|
||||
out_raw_indices=out_raw_indices,
|
||||
)
|
||||
|
||||
if not fuse_topk:
|
||||
top_k_page_table_transform.assert_not_called()
|
||||
top_k.assert_called_once()
|
||||
call = top_k.call_args
|
||||
self.assertTrue(call.args[0].is_contiguous())
|
||||
self.assertEqual(call.args[0].shape, scores.shape)
|
||||
self.assertEqual(call.args[1], out_page_indices.shape[1])
|
||||
self.assertEqual(
|
||||
call.kwargs,
|
||||
{
|
||||
"sorted": False,
|
||||
"deterministic": True,
|
||||
"tie_break": 1,
|
||||
"dsa_graph_safe": True,
|
||||
},
|
||||
)
|
||||
continue
|
||||
|
||||
top_k.assert_not_called()
|
||||
top_k_page_table_transform.assert_called_once()
|
||||
call = top_k_page_table_transform.call_args
|
||||
self.assertIs(call.args[0], scores)
|
||||
self.assertIsNot(call.args[1], page_tables)
|
||||
self.assertTrue(call.args[1].is_contiguous())
|
||||
self.assertTrue(torch.equal(call.args[1], page_tables))
|
||||
self.assertIs(call.args[2], seq_lens)
|
||||
self.assertEqual(call.args[3], out_page_indices.shape[1])
|
||||
self.assertEqual(
|
||||
call.kwargs,
|
||||
{
|
||||
"deterministic": True,
|
||||
"tie_break": 1,
|
||||
"dsa_graph_safe": True,
|
||||
"page_size": 64,
|
||||
"out": out_page_indices,
|
||||
"out_raw_indices": out_raw_indices,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TestDSV4NonPagedIndexer(CustomTestCase):
|
||||
def _is_eligible(self, **overrides):
|
||||
|
||||
Reference in New Issue
Block a user