Skip to content

[Perf][GLM-5.3-Flash] Shard long-context indexer prefill rows across TP - #54951

Open
zigzagcai wants to merge 14 commits into
vllm-project:mainfrom
zigzagcai:newly-optimize-GLM-5.3-Flash-long-context
Open

zigzagcai wants to merge 14 commits into
vllm-project:mainfrom
zigzagcai:newly-optimize-GLM-5.3-Flash-long-context

Conversation

@zigzagcai

@zigzagcai zigzagcai commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

How it works / why it is faster: Instead of repeating sparse-indexer prefill MQA scoring and top-k on every TP rank, partition independent query rows into cost-balanced contiguous slices. Each rank scores its rows against the full available key history; all_gatherv reassembles the final int32 indices. This removes redundant computation in exchange for fixed-width index communication, so savings grow with context length. There is no attention approximation. Decode kernels are unchanged; smaller batches retain the existing path (activation requires at least 16,384 × TP scheduled prefill query rows).

Performance, baseline → optimized: main parent 5893426b88f7 → PR head 16934c6e76e5; GLM-5.3-Flash on 4× H200, TP4, C1, 256 output tokens, 65,536-token batch budget, legacy runner + explicit FA3 backend. Four fresh-process A/B pairs in ABBA/BAAB order; warmups excluded. Throughput below counts input + output tokens at C1, not saturated serving throughput.

Input context Server TTFT, seconds (B → O) TTFT reduction Total tok/s (B → O) Throughput increase
128K 4.782 → 4.605 3.69% 20,269.73 → 20,835.80 2.79%
256K 10.154 → 9.372 7.70% 22,090.93 → 23,651.89 7.07%
512K 22.760 → 19.418 14.68% 21,379.03 → 24,752.65 15.78%
1M (1,000,000) 54.154 → 40.388 25.42% 17,856.28 → 23,675.18 32.59%

Server-side TPOT at 1M: 6.631 → 6.633 ms/token. Across the full 1K–1M scan, absolute TPOT percentage-change point estimates are <0.04%; every paired 95% interval includes zero. This does not prove equivalence. The 2K TTFT point estimate is 2.84% slower (interval crosses zero); we do not claim blanket short-input non-regression.

Correctness / targeted accuracy, baseline → optimized: Across 33 distinct retrieval probes spanning 1K–1M, repeated over four processes per arm, expected-answer presence is 132/132 → 132/132; strict answer-only accuracy is 127/132 → 129/132. Exact generated-token agreement is 130/132 A/B (baseline A/A: 98/99). Both observed A/B differences contain the same correct code, with an additional explanatory sentence in baseline. These are targeted retrieval checks, not a broad quality benchmark or a claim of bitwise equivalence. The 54 focused PR tests also pass.

Full eleven-length results, paired confidence intervals, implementation scope, and reproducibility details follow.

Summary

Shard replicated sparse-indexer prefill query rows across tensor-parallel ranks. Each rank computes MQA logits and top-k for a cost-balanced contiguous slice, then one variable-size all_gatherv call per indexer layer restores the original row layout. This removes redundant scoring; it does not approximate attention or merge independent top-k candidates. The API call is not a claim of one underlying physical NCCL operation.

For GLM-5.3-Flash, the exchange includes the incomplete k-pool tail: index_topk + index_kpool - 1 = 2048 + 4 - 1 = 2,051 int32 columns. KV gathering remains unconditional because subsequent query chunks can reuse that workspace.

The avoided MQA scoring work grows with both query rows and the available pooled-key history, whereas the exchanged index width per query row stays fixed. Longer histories therefore offer more computation to save per byte exchanged. The cost-balanced partition also accounts for different causal-history lengths across rows. This targets prefill TTFT and prefill-dominated total-token throughput; the row partition and exchange are outside the decode branch, so a direct decode-kernel speedup is not expected. Indirect TPOT changes are measured and reported below.

Scope and activation

The diff against main is four files: the shared DSA indexer metadata/planner, the generic sparse indexer, the GLM k-pool indexer, and their focused tests. #53906 is already in the baseline; this PR does not reintroduce its model support or kernel changes.

  • Requires CUDA, TP > 1, an MQA prefill, and at least 16,384 × TP scheduled prefill query rows in the current batch.
  • DCP/PCP, non-CUDA platforms, disabled PyNCCL, NCCL symmetric-memory, batch-invariant mode, and configurations whose mixed CUDA-graph mode is FULL retain the existing path.
  • Decode kernels, model weights, sampling, KV-cache format, and attention's mathematical definition are not changed. TPOT is nevertheless measured to check for indirect effects.
  • The shared generic DSA path is also modified. H200 serving results below cover GLM-5.3-Flash only; they are not an end-to-end no-regression guarantee for other models, platforms, concurrency levels, or speculative decoding.

Important configuration constraint: at TP4, max_num_batched_tokens=8192 cannot activate this implementation, even with a 1M-token accumulated context. Both arms below use 65,536, so C1 inputs of 1K–32K exercise the fallback, and full 64K prefill chunks can exercise sharding. The gate is a batch-row count, not the request's context length; several shorter concurrent requests can also reach it.

Exact-head A/B rescan

These measurements replace the previous historical tables. Those tables used older PR/runtime overlays and an 8,192-token batch budget; their gains cannot be attributed to the current head and gate.

Arm Source commit
Baseline main at 5893426b88f7b3cd21101d194eb1c6f0a6f0e27b, the optimized commit's parent
Optimized 16934c6e76e508b22037d5d4040488d490090044

Both arms load their respective clean source trees, the same model snapshot (3f1971b7b5f7a528c9c4ef6212c8785298a8c24a), and identical compiled vLLM extensions from the official wheel for the baseline commit. Worker-level provenance checks verify the loaded vLLM paths for all four TP ranks. The historical vendor-vLLM Python overlay is not used. The PR range contains one commit.

Configuration and stability protocol

  • 4 × NVIDIA H200, TP4, native FP8 weights, BF16 KV cache; text-only, legacy model runner (VLLM_USE_V2_MODEL_RUNNER=0) and explicit FLASH_ATTN_MLA_SPARSE attention backend, chunked prefill, max_model_len=1,048,576, max_num_batched_tokens=65,536, max_num_seqs=8, GPU-memory utilization 0.90.
  • MTP/speculative decoding off; prefix caching off; FULL_AND_PIECEWISE CUDA-graph configuration with main's default breakable-graph behavior. The effective engine configuration reports CompilationMode.NONE and CUDA-graph capture sizes [1, 2, 4, 8, 16]; this is not a torch.compile-enabled prefill benchmark. Torch 2.13.0+cu130, Triton 3.7.1, FlashInfer 0.6.18, Transformers 5.12.1. Same process environment/cache policy in both arms; OMP/MKL threads fixed at 1. Synchronous CUDA debugging is off for timed runs. These explicit runner/backend settings avoid failures observed in the unmodified baseline; see limitations below.
  • C1 sequential requests. Eight fresh serving processes in ABBA / BAAB order form four adjacent baseline/optimized restart pairs. Each process scans all eleven lengths, with three excluded warmups and eight measured requests per length. This is 32 measured requests per arm per length, not 32 independent restarts.
  • Same exact-token prompts, seeds, and order in both arms; temperature 0, top-p 1, top-k 1, fixed 256 output tokens, ignore_eos=true. 1M means 1,000,000 input tokens, leaving room for generation within the server limit. Other K labels are powers of 1,024.
  • GPU utilization, memory, clocks, temperature, and power are recorded at 1 Hz. Startup/JIT warmup and separate diagnostic instrumentation are excluded from measured samples. No outlier samples are silently removed.
  • TTFT/TPOT use the server's per-request timestamps. TTFT starts at scheduling, excluding upload and queue time. TPOT = (last output timestamp − first output timestamp) / 255. Client-side latency is retained as a transport cross-check; SSE chunk arrival intervals are not substituted for decode time.
  • Latencies below are geometric means of the four per-restart request medians. Throughput is total input + output tokens divided by the observed C1 batch interval (including client/request overhead), geometrically averaged across restarts. It is not saturated multi-client serving throughput. At a fixed input/output length, output-token throughput has the same percentage change.
  • Positive improvement means lower latency (1 − optimized/baseline) or higher throughput (optimized/baseline − 1). Confidence intervals resample the four restart pairs, not individual requests (20,000 bootstrap draws). Four pairs give limited statistical resolution; an interval crossing zero is not proof of equivalence.

Performance

Context B TTFT ms O TTFT ms TTFT improvement B TPOT ms O TPOT ms TPOT improvement B total tok/s O total tok/s Throughput improvement
1K 152.69 152.04 +0.43% 6.323 6.324 -0.02% 723.96 723.82 -0.02%
2K 151.39 155.69 -2.84% 6.468 6.469 -0.02% 1277.30 1273.71 -0.28%
4K 167.71 168.73 -0.60% 6.475 6.476 -0.02% 2387.95 2385.88 -0.09%
8K 298.48 298.25 +0.08% 6.478 6.479 -0.01% 4322.71 4322.41 -0.01%
16K 575.13 575.20 -0.01% 6.498 6.499 -0.02% 7436.45 7435.08 -0.02%
32K 1150.58 1150.46 +0.01% 6.428 6.429 -0.01% 11799.75 11800.18 +0.00%
64K 2315.76 2280.78 +1.51% 6.452 6.452 -0.00% 16549.38 16693.37 +0.87%
128K 4781.71 4605.30 +3.69% 6.557 6.557 +0.01% 20269.73 20835.80 +2.79%
256K 10154.19 9371.82 +7.70% 6.571 6.573 -0.03% 22090.93 23651.89 +7.07%
512K 22759.85 19417.85 +14.68% 6.603 6.604 -0.01% 21379.03 24752.65 +15.78%
1M (1,000,000) 54154.13 40387.57 +25.42% 6.631 6.633 -0.02% 17856.28 23675.18 +32.59%

At 1M, measured TTFT improves by +25.42% and C1 total-token throughput by +32.59%. Across all lengths, TPOT improvement point estimates range from -0.03% to +0.01%. The fallback 1K–32K TTFT changes range from -2.84% to +0.43%. The intervals below and individual restart results, not just the sign of a small point estimate, determine how confidently an improvement or regression can be distinguished from run-to-run variation. These results do not prove zero overhead outside the tested configuration. Every TPOT interval includes zero: this scan does not establish a systematic TPOT change, nor does it prove exact equivalence.

Short-input caveat: the most negative fallback TTFT point is 2K: 151.39 → 155.69 ms (-2.84% improvement; paired 95% interval [-8.18, +2.24]%). This observation is retained, not discarded or declared harmless solely because the new collective is gated off. The scan does not establish blanket short-input non-regression or a causal explanation for this timing difference.

95% paired-restart bootstrap intervals for percentage improvements
Context TTFT improvement CI TPOT improvement CI Throughput improvement CI
1K [-3.47, +4.57]% [-0.20, +0.20]% [-0.43, +0.34]%
2K [-8.18, +2.24]% [-0.19, +0.20]% [-0.70, +0.04]%
4K [-1.85, +0.28]% [-0.21, +0.18]% [-0.29, +0.12]%
8K [-0.10, +0.21]% [-0.22, +0.19]% [-0.19, +0.15]%
16K [-0.06, +0.03]% [-0.20, +0.19]% [-0.17, +0.14]%
32K [-0.01, +0.03]% [-0.23, +0.22]% [-0.11, +0.13]%
64K [+1.40, +1.58]% [-0.19, +0.22]% [+0.73, +0.99]%
128K [+3.64, +3.72]% [-0.18, +0.23]% [+2.70, +2.87]%
256K [+7.64, +7.76]% [-0.23, +0.19]% [+6.99, +7.14]%
512K [+14.61, +14.73]% [-0.21, +0.21]% [+15.68, +15.85]%
1M (1,000,000) [+25.38, +25.45]% [-0.20, +0.20]% [+32.52, +32.64]%

The four individual adjacent-restart improvements are shown in acquisition order (AB, BA, BA, AB):

Context TTFT: four paired improvements TPOT: four paired improvements Throughput: four paired improvements
1K +6.79%, +1.49%, -2.39%, -4.57% -0.06%, -0.06%, +0.29%, -0.25% +0.45%, +0.05%, +0.01%, -0.59%
2K +1.55%, -8.81%, -7.55%, +2.91% -0.04%, -0.09%, +0.29%, -0.24% +0.10%, -0.93%, -0.27%, -0.03%
4K +0.48%, -2.40%, -0.22%, -0.30% -0.02%, -0.04%, +0.25%, -0.27% +0.02%, -0.26%, +0.21%, -0.32%
8K +0.20%, +0.21%, +0.10%, -0.20% -0.01%, -0.01%, +0.26%, -0.29% +0.03%, +0.01%, +0.20%, -0.26%
16K +0.03%, +0.03%, -0.03%, -0.08% -0.09%, +0.00%, +0.29%, -0.27% -0.05%, -0.01%, +0.21%, -0.22%
32K -0.01%, -0.02%, +0.03%, +0.04% -0.01%, -0.05%, +0.31%, -0.30% -0.02%, +0.01%, +0.17%, -0.15%
64K +1.55%, +1.55%, +1.59%, +1.35% -0.02%, -0.03%, +0.30%, -0.25% +0.88%, +0.90%, +1.02%, +0.67%
128K +3.73%, +3.71%, +3.70%, +3.62% +0.07%, -0.10%, +0.34%, -0.26% +2.85%, +2.77%, +2.90%, +2.65%
256K +7.71%, +7.73%, +7.77%, +7.61% -0.01%, -0.13%, +0.30%, -0.30% +7.05%, +7.09%, +7.17%, +6.95%
512K +14.70%, +14.74%, +14.72%, +14.58% +0.00%, -0.08%, +0.30%, -0.28% +15.81%, +15.87%, +15.82%, +15.63%
1M (1,000,000) +25.45%, +25.45%, +25.43%, +25.36% -0.05%, -0.08%, +0.29%, -0.24% +32.65%, +32.63%, +32.59%, +32.48%
Client-side latency cross-check (same samples)
Context Client TTFT ms (B → O) Improvement Client TPOT ms (B → O) Improvement
1K 155.727 → 155.231 +0.32% 6.319 → 6.320 -0.02%
2K 154.952 → 159.511 -2.94% 6.463 → 6.464 -0.02%
4K 172.402 → 173.394 -0.57% 6.467 → 6.468 -0.02%
8K 305.211 → 305.051 +0.05% 6.464 → 6.465 -0.01%
16K 586.602 → 586.582 +0.00% 6.470 → 6.472 -0.03%
32K 1171.128 → 1170.227 +0.08% 6.375 → 6.379 -0.06%
64K 2352.334 → 2318.432 +1.44% 6.354 → 6.354 -0.00%
128K 4853.944 → 4679.241 +3.60% 6.352 → 6.346 +0.09%
256K 10296.507 → 9504.272 +7.69% 6.154 → 6.199 -0.72%
512K 23016.618 → 19658.953 +14.59% 5.884 → 5.938 -0.92%
1M (1,000,000) 54599.813 → 40845.645 +25.19% 5.400 → 5.378 +0.40%

Client TTFT includes request upload/processing and transport. Server-side TPOT is the primary decode measure because clients can receive multiple generated tokens in a single SSE chunk, especially after very long prefills.

Correctness / targeted accuracy A/B

This is a targeted long-context retrieval and numerical-equivalence evaluation, not a general model-quality benchmark. Three distinct generated record probes per length place the answer at 10%, 50%, and 90% of the prompt. Each is repeated in the four independent processes per arm: 12 observations per arm per length, but only three distinct tasks. The prompt has a non-thinking assistant boundary; greedy decoding stops normally with at most 64 output tokens.

The table reports strict expected-answer accuracy, whether the expected answer is present, and exact generated-token equality for matched prompts. Answer containment alone is a weaker check: a response can contain the correct code yet fail strict answer-only formatting by adding an explanatory sentence. Baseline A/A compares adjacent independent baseline restarts, providing a control for process-to-process repeatability. HTTP success and valid token counts alone are not treated as accuracy.

Context Baseline strict answer Optimized strict answer B answer present O answer present A/B exact tokens Baseline A/A exact tokens
1K 12/12 12/12 12/12 12/12 12/12 9/9
2K 12/12 12/12 12/12 12/12 12/12 9/9
4K 12/12 12/12 12/12 12/12 12/12 9/9
8K 12/12 12/12 12/12 12/12 12/12 9/9
16K 12/12 12/12 12/12 12/12 12/12 9/9
32K 12/12 12/12 12/12 12/12 12/12 9/9
64K 12/12 12/12 12/12 12/12 12/12 9/9
128K 12/12 12/12 12/12 12/12 12/12 9/9
256K 12/12 12/12 12/12 12/12 12/12 9/9
512K 8/12 9/12 12/12 12/12 11/12 9/9
1M (1,000,000) 11/12 12/12 12/12 12/12 11/12 8/9

Matched A/B output-token sequences agree in 130/132 comparisons; baseline A/A agrees in 98/99. The expected answer is present in 132/132 baseline and 132/132 optimized responses. Strict answer-only format passes 127/132 baseline and 129/132 optimized responses. Answer containment does not excuse extra or conflicting text; strict answer-only formatting and full token equality are reported separately. Repeated observations of the same three tasks are not independent evidence of broad model accuracy. Multimodal inputs, MTP, other DSA models, and general reasoning quality have not been evaluated by this scan.

Inspection of both A/B mismatches (the 90%-depth task at 512K and 1M) shows the same correct access code in both responses: baseline adds an explanatory sentence, while optimized returns only the code. The baseline A/A mismatch at 1M has the same formatting distinction; optimized's own restarts also differ in formatting at 512K. No different retrieved code was observed in these probes. This is an inspection of the observed mismatches, not a claim that all possible numerical differences are formatting-only.

Additional checks:

  • 54 focused PR tests pass: row-cost balancing, gate predicates, metadata construction, and gather/reassembly coverage including 128K–1M. These tests include mocked collective/metadata checks; they are not a substitute for the distributed serving run.
  • A separate instrumented TP4 pilot confirmed actual 64K/128K sharded execution and the [65,536, 2,051] gathered result. Its timings are excluded from the performance table.
  • Real H200 DeepGEMM + CUDA top-k checks at nine lengths from 4K to 1M compare a 512-query-row batch against uneven row slices. Valid logits are exactly equal and selected top-k sets match. The top-k kernel's output ordering is not deterministic: different orders also occur in the baseline A/A control with identical sets. These checks do not establish bitwise equivalence of all attention outputs.
  • Real Triton pool expansion plus incomplete-tail append agrees exactly with its direct reference at those nine lengths. Measurement-harness tests validate empty-text token accounting, bundled SSE responses, restart-level aggregation, and rejection of unmatched prompts or inconsistent token counts, fingerprints, and timing denominators.

Runtime qualification / limitations: all 704 measured performance requests complete with exactly 256 output tokens; all 264 separate retrieval requests complete. This is not an error-free-runtime claim: the common FA3 runtime emits TMA-descriptor diagnostics, which are preserved in the server logs. Two separate baseline failures were investigated before fixing the common benchmark configuration: (1) the automatically selected FlashInfer SM90 sparse MLA backend fails during 65,536-row startup/autotune; a standalone NoPE MLA probe also fails at 65,536 rows while passing 64 rows; (2) with FA3, the V2 runner fails at 256K in its slot-mapping kernel, reproduced with eager execution and CUDA_LAUNCH_BLOCKING=1. The legacy-runner/FA3 diagnostic then passes 256K, 512K, and 1M. Failed and synchronous diagnostic admissions are excluded from the performance table. No unrelated baseline source fix is overlaid. The common user environment also registers an unrelated SGLang operator at Python startup; neither pinned vLLM source tree has a callsite for that operator. These results qualify only the explicit legacy-runner/FA3 configuration, not default V2 or auto-selected FlashInfer SM90 MLA.

Reproduction

Use clean checkouts of the two pinned commits and one matched environment. Pin the compiled artifacts to the parent-main wheel (rather than allowing each editable installation to choose a different nightly):

export VLLM_USE_PRECOMPILED=1
export VLLM_PRECOMPILED_WHEEL_LOCATION='https://wheels.vllm.ai/5893426b88f7b3cd21101d194eb1c6f0a6f0e27b/vllm-0.28.1rc1.dev478%2Bg5893426b8-cp38-abi3-manylinux_2_28_x86_64.whl'
# In the selected arm's worktree, with common dependencies already provisioned:
uv pip install --no-deps -e .

Wheel SHA256: 93097b58f56b838b3d18928753e00b5e18624fb9084d14928e748e5fdc176dc3. The measurements used the same extracted wheel extensions in both source worktrees and checked the four workers' module paths and extension hashes. --no-deps above assumes the explicitly pinned common runtime is already installed; it is not a complete environment bootstrap.

Set MODEL_DIR to the local model snapshot, and PYTHON to the common environment's Python. Start only one arm at a time; wait for health before running requests:

export CUDA_VISIBLE_DEVICES=4,5,6,7
export VLLM_USE_V2_MODEL_RUNNER=0 CUDA_LAUNCH_BLOCKING=0
export OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 TOKENIZERS_PARALLELISM=false
export VLLM_NO_USAGE_STATS=1
# Use the selected source tree's bundled DeepGEMM, not an unrelated installed copy.
export PYTHONPATH="${PWD}/vllm/third_party:${PWD}${PYTHONPATH:+:${PYTHONPATH}}"
"$PYTHON" -m vllm.entrypoints.cli.main serve "$MODEL_DIR" \
  --host 127.0.0.1 --port 8010 --served-model-name zai-org/GLM-5.3-Flash \
  --tensor-parallel-size 4 --kv-cache-dtype bfloat16 \
  --max-model-len 1048576 --max-num-batched-tokens 65536 --max-num-seqs 8 \
  --gpu-memory-utilization 0.90 --safetensors-load-strategy prefetch \
  --enable-chunked-prefill --no-enable-prefix-caching --language-model-only \
  --attention-backend FLASH_ATTN_MLA_SPARSE \
  --tool-call-parser glm47 --reasoning-parser glm45 --enable-auto-tool-choice \
  --enable-per-request-metrics \
  --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE"}'

Use fresh process tags 00-baseline, 01-optimized, 02-optimized, 03-baseline, 04-optimized, 05-baseline, 06-baseline, 07-optimized. For each, run performance and then correctness with the client below; stop that owned server before starting the next. Keep the same shared compilation caches and do not include server startup in timing.

# ARM is baseline or optimized; TAG identifies this independent restart.
"$PYTHON" pr54951_measure.py --arm "$ARM" --tag "$TAG-performance" \
  --kind performance --warmups 3 --requests 8 --output-len 256 --output results
"$PYTHON" pr54951_measure.py --arm "$ARM" --tag "$TAG-correctness" \
  --kind correctness --warmups 0 --requests 3 --output-len 64 --output results
# Focused tests, run in the optimized source worktree:
"$PYTHON" -m pytest -q tests/v1/attention/test_indexer_tp_row_shard.py

Pair adjacent fresh processes and aggregate as specified above. Do not merge any diagnostic pilot or failed baseline admission into these samples. The source trees are unchanged by the benchmark; benchmark helpers are not additional PR commits.

Exact-token client and synthetic retrieval prompt generator (save as pr54951_measure.py)

Only the local model-location setting is made portable here. The tokenizer, prompt construction, seeds, token accounting, and measurement logic are the same as the measured campaign.

"""Exact-token serving measurements; raw samples are never silently discarded."""

from __future__ import annotations

import argparse
import os
import asyncio
import hashlib
import json
import statistics
import time
from pathlib import Path

import aiohttp
import numpy as np
from transformers import AutoTokenizer

ROOT = Path(__file__).resolve().parent
MODEL = Path(os.environ['MODEL_DIR'])
CONTEXTS = [1024, 2048, 4096, 8192, 16384, 32768, 65536,
            131072, 262144, 524288, 1000000]


def prompt_tokens(tokenizer, length: int, case: int, kind: str):
    """Construct exact lengths, preserving chat boundary and a complete needle.

    The synthetic retrieval probe is a smoke test, not a general quality eval.
    Needle positions rotate through 10%, 50%, and 90% of the input.
    Performance prompts use the same public, generated record workload.
    """
    seed = 54951 + case
    rng = np.random.default_rng(seed)
    key = f'record-{case:05d}-verification'
    answer = '-'.join(rng.choice(['amber', 'cedar', 'falcon', 'ivory',
                                  'maple', 'orbit', 'pearl', 'violet'], 4))
    prefix = tokenizer.encode(
        '[gMASK]<sop><|system|>You retrieve exact information from records. '
        'Answer only the requested access code, without explanation.'
        '<|user|>Read these archived records.\n', add_special_tokens=False)
    suffix = tokenizer.encode(
        f'\nEnd of records. What is the access code for {key}? '
        'Respond with only the access code.'
        '<|assistant|>\n<think>\n</think>\n', add_special_tokens=False)
    needle = tokenizer.encode(
        f'\nThe access code for {key} is {answer}.\n', add_special_tokens=False)
    # Generate ordinary-text background once; no special/random vocabulary IDs.
    paragraphs = []
    for i in range(512):
        number = int(rng.integers(100000, 999999))
        paragraphs.append(f'Archive entry {i}: shipment {number} was received '
                          'at the northern depot. The inspection confirmed '
                          'that the supplies were dry and accounted for.\n')
    background = tokenizer.encode(''.join(paragraphs), add_special_tokens=False)
    available = length - len(prefix) - len(suffix) - len(needle)
    assert available > 0
    background = (background * ((available // len(background)) + 1))[:available]
    depth = [0.1, 0.5, 0.9][case % 3]
    position = int(available * depth)
    tokens = prefix + background[:position] + needle + background[position:] + suffix
    assert len(tokens) == length
    digest = hashlib.sha256(np.asarray(tokens, dtype='<i4').tobytes()).hexdigest()
    return tokens, {'sha256': digest, 'case': case, 'seed': seed, 'kind': kind,
                    'needle_depth': depth, 'expected_answer': answer}


async def request(session, base_url, tokens, output_len, metadata, quality=False):
    body = {
        'model': 'zai-org/GLM-5.3-Flash', 'prompt': tokens,
        'max_tokens': output_len, 'temperature': 0, 'top_p': 1, 'top_k': 1,
        'seed': 54951, 'ignore_eos': not quality, 'stream': True,
        'stream_options': {'include_usage': True},
        # Streaming logprobs identify empty-text tokens without echoing a
        # million prompt IDs in the first response chunk.
        'logprobs': 5 if quality else 0, 'return_tokens_as_token_ids': True,
        'skip_special_tokens': False,
    }
    # Serialize outside the timed interval. HTTP upload remains in client TTFT.
    payload = json.dumps(body, separators=(',', ':')).encode()
    record = {**metadata, 'input_tokens': len(tokens), 'requested_output': output_len,
              'started_unix': time.time(), 'chunks': [], 'output_token_ids': [],
              'text': '', 'logprobs': [], 'server_metrics': None}
    started = time.perf_counter()
    async with session.post(base_url + '/v1/completions', data=payload,
                            headers={'Content-Type': 'application/json'}) as response:
        if response.status != 200:
            raise RuntimeError(f'HTTP {response.status}: {(await response.text())[:2000]}')
        async for raw_line in response.content:
            received = time.perf_counter() - started
            line = raw_line.decode().strip()
            if not line.startswith('data: '):
                continue
            if line == 'data: [DONE]':
                break
            event = json.loads(line[6:])
            if 'error' in event:
                raise RuntimeError(event['error'])
            if event.get('metrics'):
                record['server_metrics'] = event['metrics']
            if event.get('usage'):
                record['usage'] = event['usage']
            for choice in event.get('choices', []):
                logprobs = choice.get('logprobs') or {}
                ids = [int(t.removeprefix('token_id:'))
                       for t in logprobs.get('tokens', [])]
                if ids:
                    record['chunks'].append({'t_s': received, 'n': len(ids)})
                    record['output_token_ids'].extend(ids)
                    record['logprobs'].extend(logprobs.get('token_logprobs', []))
                record['text'] += choice.get('text', '')
                if choice.get('finish_reason'):
                    record['finish_reason'] = choice['finish_reason']
    record['client_elapsed_s'] = time.perf_counter() - started
    chunks = record['chunks']
    count = len(record['output_token_ids'])
    assert chunks and count > 0, 'No identifiable output tokens received'
    assert record.get('usage', {}).get('prompt_tokens') == len(tokens), record.get('usage')
    assert record['usage']['completion_tokens'] == count, record['usage']
    if not quality:
        assert count == output_len, (count, output_len)
    record['client_ttft_ms'] = chunks[0]['t_s'] * 1000
    record['client_tpot_ms'] = ((chunks[-1]['t_s'] - chunks[0]['t_s']) * 1000 /
                              (count - 1)) if count > 1 else None
    record['output_tokens'] = count
    record['token_sha256'] = hashlib.sha256(
        np.asarray(record['output_token_ids'], dtype='<i4').tobytes()).hexdigest()
    record['retrieval_contains_answer'] = metadata['expected_answer'] in record['text']
    # The forced non-thinking boundary makes this strict answer test meaningful.
    normalized = record['text'].replace('<|endoftext|>', '').replace('<|user|>', '').strip()
    record['retrieval_exact'] = normalized == metadata['expected_answer']
    return record


async def run(args):
    output = Path(args.output)
    output.mkdir(parents=True, exist_ok=True)
    path = output / f'{args.tag}.jsonl'
    if path.exists():
        raise FileExistsError(f'Refusing to overwrite {path}')
    tokenizer = AutoTokenizer.from_pretrained(str(MODEL), trust_remote_code=False)
    timeout = aiohttp.ClientTimeout(total=1800, connect=10)
    async with aiohttp.ClientSession(timeout=timeout, trust_env=False,
                                    read_bufsize=32 * 1024 * 1024) as session:
        async with session.get(args.base_url + '/health') as response:
            assert response.status == 200
        with path.open('x') as stream:
            for length in args.contexts:
                jobs = [('warmup', i) for i in range(args.warmups)]
                jobs += [('measured', i) for i in range(args.requests)]
                prepared = {}
                for phase, i in jobs:
                    case = 1000 + i if phase == 'warmup' else i
                    prepared[phase, i] = prompt_tokens(tokenizer, length, case, args.kind)
                batch_start = None
                batch_input = batch_output = 0
                for phase, i in jobs:
                    # Same measured prompts and order in every fresh process.
                    tokens, metadata = prepared[phase, i]
                    metadata.update(arm=args.arm, tag=args.tag, phase=phase,
                                    context=length, repetition=i)
                    if phase == 'measured' and batch_start is None:
                        batch_start = time.perf_counter()
                    record = await request(session, args.base_url, tokens,
                                           args.output_len, metadata,
                                           quality=args.kind == 'correctness')
                    if phase == 'measured':
                        batch_input += len(tokens)
                        batch_output += record['output_tokens']
                        batch_end = time.perf_counter()
                    stream.write(json.dumps(record, separators=(',', ':')) + '\n')
                    stream.flush()
                    metrics = record['server_metrics'] or {}
                    print(json.dumps({'tag': args.tag, 'context': length, 'phase': phase,
                                      'i': i, 'client_ttft': record['client_ttft_ms'],
                                      'server': metrics, 'tokens': record['output_tokens'],
                                      'answer': record['retrieval_contains_answer']}), flush=True)
                if batch_start is not None:
                    summary = {'record_type': 'batch_summary', 'context': length,
                               'arm': args.arm, 'tag': args.tag, 'kind': args.kind,
                               'duration_s': batch_end - batch_start,
                               'input_tokens': batch_input, 'output_tokens': batch_output,
                               'total_tok_s': (batch_input + batch_output) / (batch_end - batch_start),
                               'output_tok_s': batch_output / (batch_end - batch_start)}
                    stream.write(json.dumps(summary) + '\n')
                    stream.flush()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--arm', choices=['baseline', 'optimized'], required=True)
    parser.add_argument('--tag', required=True)
    parser.add_argument('--output', default=str(ROOT / 'results/pr54951-current-20260907'))
    parser.add_argument('--base-url', default='http://127.0.0.1:8010')
    parser.add_argument('--contexts', nargs='+', type=int, default=CONTEXTS)
    parser.add_argument('--warmups', type=int, default=3)
    parser.add_argument('--requests', type=int, default=8)
    parser.add_argument('--output-len', type=int, default=256)
    parser.add_argument('--kind', choices=['performance', 'correctness'], default='performance')
    asyncio.run(run(parser.parse_args()))

AI assistance and ownership

AI assistance was used for implementation and benchmark analysis. The author owns validation, review follow-up, and maintenance. This PR optimizes the already-merged GLM/DSA prefill path; it is not a duplicate model-support submission.

@mergify

mergify Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @zigzagcai.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 2, 2026
@zigzagcai zigzagcai closed this Sep 2, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in AMD Sep 2, 2026
@github-project-automation github-project-automation Bot moved this from To triage to Done in torch.compile integration Sep 2, 2026
@github-project-automation github-project-automation Bot moved this to Done in NVIDIA Sep 2, 2026
@zigzagcai zigzagcai reopened this Sep 3, 2026
@github-project-automation github-project-automation Bot moved this from Done to Backlog in torch.compile integration Sep 3, 2026
@mergify

mergify Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @zigzagcai.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@zigzagcai zigzagcai changed the title [Perf][GLM5] Shard long-context indexer prefill rows across TP [Perf][GLM-5.3-Flash] Shard long-context indexer prefill rows across TP Sep 3, 2026
Resolve conflict in vllm/models/glm5next/nvidia/sparse_indexer.py
(formerly model_executor/layers/sparse_attn_indexer_kpool.py, moved
and AMD-split by vllm-project#55358): keep upstream's post-refactor NVIDIA code
path and re-apply the TP row-shard slicing (row_start/row_end,
sliced cu_seqlen_ks/ke, all_gatherv exchange) on top.

Signed-off-by: Zheng Cai <8370601+zigzagcai@users.noreply.github.com>
@mergify

mergify Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Hi @zigzagcai, the pre-commit checks have failed. Please run:

uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work!

Please do the following:

  • shrink the diff, idealy < 500 LOC
  • e2e accuracy using lm_eval...
  • e2e benchmark using vllm bench serve...

When doing e2e test, please attach with full output log in case some agents fake the data.

@zigzagcai

zigzagcai commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @yewentao256

Thanks for the invaluable feedback!

  1. For the diff part,

The diff of this PR's functionality is actually <500 LOC, and the majority of diff lines is the added unit test file.

File Additions Deletions Total diff
tests/v1/attention/test_indexer_tp_row_shard.py 832 0 832
vllm/model_executor/layers/sparse_attn_indexer.py 55 13 68
vllm/models/glm5next/nvidia/sparse_indexer.py 67 22 89
vllm/v1/attention/backends/mla/indexer.py 163 7 170
Total 1,117 42 1,159
  1. For the validation part,

I have added the e2e accuracy/benchmarking details and logs, with the latest upstream baseline 3059155b47cee5269eaf446e24c9d0f8f908c7c9 and latest PR commit f0fc8269fbea535b9bae87eb8d021b37682e5cbb.

E2E accuracy and benchmark (FlashInfer cubin enabled)

I reran the Model Runner V2 + SpecDecode comparison after installing and enabling the FlashInfer cubin artifacts. Both revisions used the same GLM-5.3-Flash weights, 4x NVIDIA H200 GPUs, MTP with 5 speculative tokens, --max-model-len 1048576, and the same serving configuration.

  • Baseline: 3059155b47cee5269eaf446e24c9d0f8f908c7c9
  • Optimized: f0fc8269fbea535b9bae87eb8d021b37682e5cbb
  • Model: /mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash
  • FlashInfer: flashinfer-python==0.7.0, flashinfer-cubin==0.7.0
  • VLLM_HAS_FLASHINFER_CUBIN=1; VLLM_BLOCKSCALE_FP8_GEMM_FLASHINFER=0 was not set.
  • Each benchmark point is 3 serial requests, 256 generated tokens, temperature 0, with full per-request JSON and stdout/stderr logs attached.
Context Baseline TTFT Optimized TTFT TTFT reduction Baseline output throughput Optimized output throughput Throughput gain
256K 10.661 s 9.873 s 7.39% 22.52 tok/s 24.17 tok/s 7.32%
512K 24.053 s 20.486 s 14.83% 10.42 tok/s 12.21 tok/s 17.22%
1M 58.203 s 43.044 s 26.05% 4.37 tok/s 5.90 tok/s 35.00%

Long-context accuracy (lm_eval)

I added a retrieval task with six samples at each context length. Each sample places an exact eight-digit code at a different depth in a 256K, 512K, or 1M-token prompt. The task was run through the OpenAI-compatible vLLM endpoint with tokenized_requests=false, temperature=0, and max_length=1000100.

Context Baseline exact match Optimized exact match
256K 6/6 (1.0) 6/6 (1.0)
512K 6/6 (1.0) 6/6 (1.0)
1M 6/6 (1.0) 6/6 (1.0)

Full output logs and machine-readable results are available here: public gist. The gist contains full-output.log (complete server, benchmark, and lm_eval output), comparison.json, and this report. Download the complete archive, including per-request JSON files, benchmark server logs, task definitions, and lm_eval samples: pr54951-flashinfer-logs.tar.gz.

@jin-amd

jin-amd commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for this — it ports cleanly to ROCm, and it pays off there too, at a much lower threshold.

ROCm port: jin-amd@cc0609a — one commit on top of this branch's head, +73/−26. glm5next/amd/sparse_indexer.py runs the same chunk loop as the NVIDIA indexer, so the change mirrors yours: clip each chunk to the rank's shard after the (unconditional) K gather, score only those rows, then one all_gatherv per layer. all_gatherv already exists on the CUDA-alike communicator ROCm uses, so nothing else is needed. In indexer.py the gate becomes is_cuda_alike(), and the two gate tests patch is_cuda_alike instead of is_cuda to match.

On the threshold question: on MI325X at TP4 the replicated kpool indexer costs ~58 ms of a ~663 ms 16K-token prefill chunk (fp8_mqa_logits ~29 ms + top_k_per_row_prefill ~28 ms), so ROCm starts sharding at 2048 rows per rank. At max_num_batched_tokens=16384, the 16384-per-rank threshold never triggers there.

Measured with vllm bench serve and lm_eval — MI325X, TP4, GLM-5.3-Flash FP8, 131072 in / 1024 out, 7 concurrency points × 20 prompts, arms differing only in the threshold:

sharded vs. replicated
output throughput +2.57% (7/7 rows)
TPOT +3.09% (7/7)
TTFT +1.99% (6/7)
gsm8k strict 0.9757 → 0.9742

It also runs with MTP (k=5) on ROCm: long prompts shard and complete without errors.

To keep this PR's diff down (per @yewentao256), I'll open it as a follow-up once this lands — unless you'd rather take it here.

Two smaller notes:

  • After the latest merge of main, three tests in tests/v1/attention/test_indexer_tp_row_shard.py fail with ModuleNotFoundError: No module named 'vllm.model_executor.layers.sparse_attn_indexer_kpool' — main split that module into vllm/models/glm5next/{amd,nvidia}/sparse_indexer.py. They are test_kpool_sharded_prefill_exchanges_expanded_tail_and_excludes_padding and both test_sharded_prefill_preserves_padding_and_kpool_tail cases.
  • Profiling the sharded run, the all-gather adds ~21 ms of RCCL per 16K chunk, because the exchanged rows are the expanded indices (16384 × 2051 int32 = 134 MB per layer). Exchanging the pool ids instead (33.5 MB) and expanding after the gather gave another +4.36% TTFT and +2.01% throughput on MI325X (7/7 rows, both arms also carrying [ROCm][Perf][GLM-5.3-Flash] "Fit kpool top-k indices to AITER" with a single Triton kernel #58008), with bitwise-identical indices. The NVIDIA indexer does the same expanded exchange, so it may carry over — happy to send that as a separate follow-up too.

@yewentao256

Copy link
Copy Markdown
Member

@zigzagcai There are still a lot of code including unit tests could be simplified from my view.

Also, as agents may generated fake results sometimes, please just copy paste full command line and the raw output here.

root and others added 2 commits September 28, 2026 14:44
Reduce redundant row-sharding tests and comments while retaining core correctness and gate coverage.

Co-authored-by: OpenAI Codex <noreply@openai.com>
The metadata builder uses its class-level chunking implementation after the PCP/DCP refactor.

Co-authored-by: OpenAI Codex <noreply@openai.com>
@zigzagcai

zigzagcai commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

@zigzagcai There are still a lot of code including unit tests could be simplified from my view.

Also, as agents may generated fake results sometimes, please just copy paste full command line and the raw output here.

Hi @yewentao256 , thanks for the careful review!

Firstly, this PR's optimziation was driven and discovered by my-developed end-to-end autonomous evolution system through a loop of profiling, experimentation, and verifier feedback.

I understand your concern that agents may sometimes generate fake results. We've done a lot of work to prevent reward hacking from verification system(for example, adversarial agent to perform verification, since the implementation agent and adversarial agent usually have different training data distribution, and some human developed verification framework to improve benchmark numerical stability and detect code that might generate fake results, using the techniques like AST/roofline model/...). And we also integrate loopx framework to prevent goal drift in this kind of long-horizon tasks. Besides, I also manually reviewed the implementation logic behind the optimization to ensure that it wasn’t just AI-generated sloppy code, but a valuable optimization.

Secondly, for the follow-up questions:

  1. I have simplified the code including unittest in commit 76f95ce and 0b29cd1
  2. I see @jin-amd has ported this optimization to AMD ROCm platform in commit jin-amd@cc0609a, and verified the correctness and effectiveness independently.
  3. For the reproducible full command line and raw output
PR #54951 latest commit 1M context test record
Commit: 0b29cd16410c1cf7edfa75098706ef0b1e6da080
Date: 2026-09-28 UTC

Capture scope: complete benchmark and lm_eval client stdout/stderr, including warnings and progress output. The original server-0b29cd1.log is empty; server startup/runtime output was NOT captured to file. Only the server command is recorded below.
Model: /mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash
Hardware: 4x NVIDIA H200, CUDA_VISIBLE_DEVICES=0,1,2,3

Working directory: /root/vllm
cd /root/vllm

Environment:
export PATH=/usr/local/cuda/bin:/root/vllm/.venv/bin:$PATH
export CUDA_VISIBLE_DEVICES=0,1,2,3
export VLLM_HAS_FLASHINFER_CUBIN=1
export VLLM_USE_V2_MODEL_RUNNER=1
export CUDA_LAUNCH_BLOCKING=0
export OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 TOKENIZERS_PARALLELISM=false VLLM_NO_USAGE_STATS=1
export PYTHONPATH=/root/vllm/vllm/third_party:/root/vllm

Server command:
/root/vllm/.venv/bin/python -m vllm.entrypoints.cli.main serve /mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash \
  --host 127.0.0.1 --port 8010 --served-model-name zai-org/GLM-5.3-Flash \
  --tensor-parallel-size 4 --kv-cache-dtype bfloat16 --max-model-len 1048576 \
  --max-num-batched-tokens 65536 --max-num-seqs 8 --gpu-memory-utilization 0.90 \
  --safetensors-load-strategy prefetch --enable-chunked-prefill --no-enable-prefix-caching \
  --language-model-only --attention-backend FLASH_ATTN_MLA_SPARSE \
  --speculative-config '{"method":"mtp","num_speculative_tokens":5}' \
  --enable-per-request-metrics --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE"}'

Benchmark command:
/root/vllm/.venv/bin/python -m vllm.entrypoints.cli.main bench serve \
  --backend openai --base-url http://127.0.0.1:8010 --endpoint /v1/completions \
  --model zai-org/GLM-5.3-Flash --dataset-name random \
  --tokenizer /mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash \
  --random-input-len 1000000 --random-output-len 256 --num-prompts 3 \
  --max-concurrency 1 --request-rate inf --temperature 0 --ignore-eos --seed 54951 \
  --save-result --save-detailed --result-dir /tmp/pr54951-latest \
  --result-filename opt-0b29cd1-bench-1m.json

Full benchmark command/output (original bytes):
Mon Sep 28 07:48:09 UTC 2026
git rev-parse HEAD: 0b29cd16410c1cf7edfa75098706ef0b1e6da080
command: /root/vllm/.venv/bin/python -m vllm.entrypoints.cli.main bench serve --backend openai --base-url http://127.0.0.1:8010 --endpoint /v1/completions --model zai-org/GLM-5.3-Flash --dataset-name random --tokenizer /mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash --random-input-len 1000000 --random-output-len 256 --num-prompts 3 --max-concurrency 1 --request-rate inf --temperature 0 --ignore-eos --seed 54951 --save-result --save-detailed --result-dir /tmp/pr54951-latest --result-filename opt-0b29cd1-bench-1m.json 
Namespace(subparser='bench', bench_type='serve', dispatch_function=<function BenchmarkServingSubcommand.cmd at 0x7f524032b100>, trust_remote_code=False, seed=54951, num_prompts=3, dataset_name='random', no_stream=False, dataset_path=None, no_oversample=False, skip_chat_template=False, enable_multimodal_chat=False, disable_shuffle=False, custom_output_len=256, custom_ensure_client_side_data=False, spec_bench_output_len=256, spec_bench_category=None, sonnet_input_len=550, sonnet_output_len=150, sonnet_prefix_len=200, sharegpt_output_len=None, timed_trace_chunk_hash_size=16, timed_trace_sec_multiplier=1, timed_trace_label_timestamp='timestamp', timed_trace_label_input_length='input_length', timed_trace_label_output_length='output_length', timed_trace_label_hash_ids='hash_ids', blazedit_min_distance=0.0, blazedit_max_distance=1.0, asr_max_audio_len_sec=inf, asr_min_audio_len_sec=0.0, random_input_len=1000000, random_output_len=256, random_range_ratio='0.0', random_prefix_len=0, random_batch_size=1, no_reranker=False, random_mm_base_items_per_request=1, random_mm_num_mm_items_range_ratio=0.0, random_mm_limit_mm_per_prompt={'image': 255, 'video': 1}, random_mm_bucket_config={(256, 256, 1): 0.5, (720, 1280, 1): 0.5, (720, 1280, 16): 0.0}, hf_subset=None, hf_split=None, hf_name=None, hf_output_len=None, bfcl_categories=None, prefix_repetition_prefix_len=256, prefix_repetition_suffix_len=256, prefix_repetition_num_prefixes=10, prefix_repetition_output_len=128, speed_bench_dataset_subset='qualitative', speed_bench_output_len=4096, speed_bench_category=None, label=None, backend='openai', base_url='http://127.0.0.1:8010', host='127.0.0.1', port=8000, endpoint='/v1/completions', header=None, max_concurrency=1, model='zai-org/GLM-5.3-Flash', input_len=None, output_len=None, tokenizer='/mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash', tokenizer_mode='auto', use_beam_search=False, logprobs=None, request_rate=inf, burstiness=1.0, probe_request_rate=0.0, disable_tqdm=False, num_warmups=0, profile=False, save_result=True, save_detailed=True, append_result=False, metadata=None, result_dir='/tmp/pr54951-latest', result_filename='opt-0b29cd1-bench-1m.json', ignore_eos=True, self_timed=None, percentile_metrics=None, metric_percentiles='99', goodput=None, request_id_prefix='bench-f62ce654-', top_p=None, top_k=None, min_p=None, temperature=0.0, frequency_penalty=None, presence_penalty=None, repetition_penalty=None, served_model_name=None, lora_modules=None, lora_assignment='random', ramp_up_strategy=None, ramp_up_start_rps=None, ramp_up_end_rps=None, ready_check_timeout_sec=0, chat_template_kwargs=None, extra_body=None, skip_tokenizer_init=False, insecure=False, plot_timeline=False, timeline_itl_thresholds='25,50', plot_dataset_stats=False)
INFO 09-28 15:48:18 [utils.py:88] Sampling input_len from [1000000, 1000000] and output_len from [256, 256]
[transformers] Token indices sequence length is longer than the specified maximum sequence length for this model (1051979 > 1048576). Running this sequence through the model will result in indexing errors
Starting initial single prompt test run...
Skipping endpoint ready check.
Starting main benchmark run...
Traffic request rate: inf
Burstiness factor: 1.0 (Poisson process)
Maximum request concurrency: 1

  0%|          | 0/3 [00:00<?, ?it/s]
 33%|███▎      | 1/3 [00:43<01:26, 43.32s/it]
 67%|██████▋   | 2/3 [01:26<00:43, 43.31s/it]
100%|██████████| 3/3 [02:10<00:00, 43.35s/it]
100%|██████████| 3/3 [02:10<00:00, 43.34s/it]
tip: install termplotlib and gnuplot to plot the metrics
============ Serving Benchmark Result ============
Successful requests:                     3         
Failed requests:                         0         
Maximum request concurrency:             1         
Benchmark duration (s):                  130.03    
Total input tokens:                      3000000   
Total generated tokens:                  768       
Request throughput (req/s):              0.02      
Output token throughput (tok/s):         5.91      
Peak output token throughput (tok/s):    32.00     
Peak concurrent requests:                2.00      
Total token throughput (tok/s):          23077.89  
---------------Time to First Token----------------
Mean TTFT (ms):                          43128.95  
Median TTFT (ms):                        43081.43  
P99 TTFT (ms):                           43256.30  
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms):                          0.84      
Median TPOT (ms):                        0.90      
P99 TPOT (ms):                           1.38      
---------------Inter-token Latency----------------
Mean ITL (ms):                           10.67     
Median ITL (ms):                         10.90     
P99 ITL (ms):                            21.73     
---------------Speculative Decoding---------------
Acceptance rate (%):                     72.89     
Acceptance length:                       4.64      
Drafts:                                  166       
Draft tokens:                            830       
Accepted tokens:                         605       
Per-position acceptance (%):
  Position 0:                            98.19     
  Position 1:                            86.75     
  Position 2:                            83.73     
  Position 3:                            73.49     
  Position 4:                            22.29     
==================================================


Accuracy command:
/root/vllm/.venv/bin/lm_eval --model local-completions \
  --model_args base_url=http://127.0.0.1:8010/v1/completions,model=zai-org/GLM-5.3-Flash,tokenizer=/mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash,max_length=1000100,max_gen_toks=64,tokenized_requests=False \
  --tasks pr54951_retrieval_1m --include_path /tmp/pr54951-flashinfer/tasks \
  --limit 6 --output_path /tmp/pr54951-latest/lm-eval-0b29cd1-1m --log_samples

Full accuracy command/output (original bytes):
Mon Sep 28 07:51:40 UTC 2026
git rev-parse HEAD: 0b29cd16410c1cf7edfa75098706ef0b1e6da080
command: /root/vllm/.venv/bin/lm_eval --model local-completions --model_args base_url=http://127.0.0.1:8010/v1/completions\,model=zai-org/GLM-5.3-Flash\,tokenizer=/mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash\,max_length=1000100\,max_gen_toks=64\,tokenized_requests=False --tasks pr54951_retrieval_1m --include_path /tmp/pr54951-flashinfer/tasks --limit 6 --output_path /tmp/pr54951-latest/lm-eval-0b29cd1-1m --log_samples 
2026-09-28:15:51:40 WARNING  [config.evaluate_config:337] --limit SHOULD ONLY BE USED FOR TESTING. REAL METRICS SHOULD NOT BE COMPUTED USING LIMIT.
2026-09-28:15:51:46 INFO     [_cli.run:391] Including path: /tmp/pr54951-flashinfer/tasks
2026-09-28:15:51:46 INFO     [_cli.run:392] Selected Tasks: ['pr54951_retrieval_1m']
2026-09-28:15:51:46 INFO     [evaluator:214] Setting random seed to 0 | Setting numpy seed to 1234 | Setting torch manual seed to 1234 | Setting fewshot manual seed to 1234
2026-09-28:15:51:46 INFO     [evaluator:239] Initializing local-completions model, with arguments: {'base_url': 'http://127.0.0.1:8010/v1/completions', 'model': 'zai-org/GLM-5.3-Flash', 'tokenizer': '/mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash', 'max_length': 1000100, 'max_gen_toks': 64, 'tokenized_requests': False}
2026-09-28:15:51:47 INFO     [models.openai_completions:42] Remote tokenizer not supported. Using huggingface tokenizer backend.
2026-09-28:15:51:47 INFO     [models.api_models:176] Using max length 1000100 - 1
2026-09-28:15:51:47 INFO     [models.api_models:179] Concurrent requests are disabled. To enable concurrent requests, set `num_concurrent` > 1.
2026-09-28:15:51:47 INFO     [models.api_models:197] Using tokenizer huggingface
2026-09-28:15:51:51 INFO     [evaluator_utils:472] Selected tasks:
2026-09-28:15:51:51 INFO     [evaluator_utils:506] Task: pr54951_retrieval_1m (/tmp/pr54951-flashinfer/tasks/pr54951_retrieval_1m.yaml)
2026-09-28:15:51:51 INFO     [evaluator:314] pr54951_retrieval_1m: Using gen_kwargs: {'max_gen_toks': 64, 'temperature': 0.0, 'do_sample': False, 'until': ['\n', '</s>', '<|im_end|>']}
2026-09-28:15:51:51 INFO     [api.task:312] Building contexts for pr54951_retrieval_1m on rank 0...

  0%|          | 0/6 [00:00<?, ?it/s]
100%|██████████| 6/6 [00:00<00:00, 1810.75it/s]
2026-09-28:15:51:51 INFO     [evaluator:589] Running generate_until requests
2026-09-28:15:51:51 INFO     [models.api_models:752] Tokenized requests are disabled. Context + generation length is not checked.

Requesting API:   0%|          | 0/6 [00:00<?, ?it/s]
Requesting API:  17%|█▋        | 1/6 [00:42<03:33, 42.66s/it]
Requesting API:  33%|███▎      | 2/6 [01:25<02:51, 42.75s/it]
Requesting API:  50%|█████     | 3/6 [02:08<02:08, 42.78s/it]
Requesting API:  67%|██████▋   | 4/6 [02:51<01:25, 42.82s/it]
Requesting API:  83%|████████▎ | 5/6 [03:33<00:42, 42.82s/it]
Requesting API: 100%|██████████| 6/6 [04:16<00:00, 42.83s/it]
Requesting API: 100%|██████████| 6/6 [04:16<00:00, 42.81s/it]
2026-09-28:15:56:13 INFO     [loggers.evaluation_tracker:247] Saving results aggregated
2026-09-28:15:56:13 INFO     [loggers.evaluation_tracker:119] Saving per-task samples to /tmp/pr54951-latest/lm-eval-0b29cd1-1m/zai-org__GLM-5.3-Flash/*.jsonl
local-completions ({'base_url': 'http://127.0.0.1:8010/v1/completions', 'model': 'zai-org/GLM-5.3-Flash', 'tokenizer': '/mnt/shared-storage/models--ZhipuAI--GLM-5.3-Flash', 'max_length': 1000100, 'max_gen_toks': 64, 'tokenized_requests': False}), gen_kwargs: ({}), limit: 6.0, num_fewshot: None, batch_size: 1
|       Tasks        |Version|Filter|n-shot|  Metric   |   |Value|   |Stderr|
|--------------------|------:|------|-----:|-----------|---|----:|---|-----:|
|pr54951_retrieval_1m|      1|code  |     0|exact_match|↑  |    1|±  |     0|

@zigzagcai

zigzagcai commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

@yewentao256 , I also added table to compare computation and communication complexity, and flowchart to show the computation saving and communication overhead. Therefore the benefits of this PR can be better understood.

TL;DR — The sparse long-context indexer's prefill MQA scoring + top-k selection was redundantly computed on every TP rank. Because independent query rows can be computed separately, we shard those rows across ranks (sequence parallelism inside the indexer scoring / top-k stages) and reassemble the int32 index layout with one all_gatherv per indexer layer.

1. Before vs After (TP=4)

BEFORE — every rank computes the full result (4× redundant)
┌───────────────────────────────────────────────────────────┐
│ rank0: score ALL query rows × full pooled-key history     │
│ rank1: score ALL query rows × full pooled-key history     │  ← identical work
│ rank2: score ALL query rows × full pooled-key history     │
│ rank3: score ALL query rows × full pooled-key history     │
└───────────────────────────────────────────────────────────┘

AFTER — shard query rows, each rank scores its slice against FULL keys
┌──────────────────────────────────────────────────────────┐
│ rank0: rows [0    : N/4) × full keys   [====]            │
│ rank1: rows [N/4  : N/2) × full keys   [====]            │  ← 1/TP each
│ rank2: rows [N/2  :3N/4) × full keys   [====]            │
│ rank3: rows [3N/4 : N  ) × full keys   [====]            │
│                         ↓                                │
│   all_gatherv (once per indexer layer)                   │
│   → reassemble int32 top-k index layout                  │
│   → per row: index_topk + index_kpool - 1 cols           │
└──────────────────────────────────────────────────────────┘

2. Why it is faster — three key points

Scaling Meaning
Compute saved O(Q_rows × L_kv) MQA scoring + top-k are independent per query row, so they are row-shardable
Comm added O(Q_rows × C_idx) Only the final int32 indices are exchanged; width is fixed per row (around index_topk + index_kpool - 1 cols) and independent of context length
Net gain grows with L_kv Compute scales with context, communication does not → larger contexts benefit more
saved compute  ∝  Q_rows × L_kv                                             (rows × pooled-key history)
added comm     ∝  Q_rows × (index_topk + index_kpool - 1) × 4B              (rows × fixed index width)

⇒ speedup ↑ as context length ↑

3. Prefill data flow (optimized path)

  (1) schedule            (2) row planner           (3) local MQA score
  collect prefill    →    cost-balanced       →     this rank's rows
  query rows              contiguous slices         × FULL pooled keys
                                │                          │
                                │  (row_start / row_end)   │
                                │  sliced cu_seqlen_ks/ke  │
                                ▼                          ▼
  (6) decode unchanged  ←  (5) all_gatherv        ←  (4) local top-k
  decoder kernels           per indexer layer          int32 indices
  zero diff (exact)         reassemble layout          (+ k-pool tail)

Not an attention approximation. Every query row still attends to the full key history — only which rank computes which row changes. Decode kernels are untouched.

4. Why shard on query rows (not heads / KV)

Dimension Viable? Reason
Head parallel ✗ Indexer is MQA — head dim does not span TP
KV / sequence split ✗ Each row must score against the full key history
Query-row split ✓ Rows are independent → natural sequence parallelism

The planner is cost-balanced, not a naive equal-row split: causal history lengths differ per row, so slices are chosen so each rank does roughly the same amount of scoring work (avoids a long tail before the all_gatherv).


In one sentence: replace O(rows × ctx) of redundant prefill compute across TP ranks with O(rows) of index-only communication — the longer the context, the better the trade.

@gaby

gaby commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

I see the tests were done with H100, do these improvements also translate to Blackwell GPU's ?

@zigzagcai

zigzagcai commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

I see the tests were done with H100, do these improvements also translate to Blackwell GPU's ?

Hi @gaby ,the tests were done under H200.

I don't have Blackwell GPU at hand, but I think these improvements are generic and should works well with Blackwell.
BTW, the threshold MIN_TP_SHARD_ROWS_PER_RANK should be calibrated to Blackwell.

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Please take a look at this AI generated comment:

[P2] Avoid row sharding when the indexer takes the short-prefill path
sparse_mla_force_mqa=True makes prefill_uses_mqa true even when max_prefill_seq_len <= index_topk, so this can create row_shard_sizes for a batch where the GLM indexer later takes short_prefill. In that path the scoring loop is skipped entirely and every rank just fills the causal indices locally, but shard_sizes != None still causes the 2051-wide all_gatherv.
For example, at TP4, 32 × 2048-token prefills reach the 65,536-row threshold and would exchange ~513 MiB/layer while saving zero indexer scoring work. sparse_mla_force_mqa is also a supported workaround on SM120, so this is reachable in practice. Could we suppress row_shard_sizes whenever prefill_max_seq_len <= index_topk, irrespective of whether MQA itself was forced?

Short prefills fill all causal indices locally without MQA scoring.
Ignore row-shard sizes on this path to avoid an unnecessary all-gather
when shared metadata was built with forced MQA.

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Codex <noreply@openai.com>
@zigzagcai
zigzagcai force-pushed the newly-optimize-GLM-5.3-Flash-long-context branch from 5bd642d to eeb7409 Compare September 30, 2026 08:50
@zigzagcai

zigzagcai commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks! Please take a look at this AI generated comment:

[P2] Avoid row sharding when the indexer takes the short-prefill path
sparse_mla_force_mqa=True makes prefill_uses_mqa true even when max_prefill_seq_len <= index_topk, so this can create row_shard_sizes for a batch where the GLM indexer later takes short_prefill. In that path the scoring loop is skipped entirely and every rank just fills the causal indices locally, but shard_sizes != None still causes the 2051-wide all_gatherv.
For example, at TP4, 32 × 2048-token prefills reach the 65,536-row threshold and would exchange ~513 MiB/layer while saving zero indexer scoring work. sparse_mla_force_mqa is also a supported workaround on SM120, so this is reachable in practice. Could we suppress row_shard_sizes whenever prefill_max_seq_len <= index_topk, irrespective of whether MQA itself was forced?

Thanks for catching this!
With sparse_mla_force_mqa=True, short prefills could still carry row_shard_sizes, causing an unnecessary all_gatherv after local causal index filling.
Commit eeb7409 skips this exchange for short prefills while preserving it for long prefills.

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Could you check if this PR would affect dsv4.1? eg. indexer_sparse_logits is True

@mergify mergify Bot added deepseek Related to DeepSeek models DSv4.1 Related to DeepSeek-V4.1 models labels Oct 5, 2026
Signed-off-by: Zheng Cai <8370601+zigzagcai@users.noreply.github.com>
@zigzagcai
zigzagcai force-pushed the newly-optimize-GLM-5.3-Flash-long-context branch from 1b73618 to 2e26eac Compare October 5, 2026 15:36
@mergify

mergify Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @zigzagcai.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/build deepseek Related to DeepSeek models DSv4.1 Related to DeepSeek-V4.1 models glm kv-cache-manager kv-connector mrv2 Model Runner V2 specific multi-modality Related to multi-modality (#4194) needs-rebase new-model Requests to new models nvidia quantization rocm Related to AMD ROCm scheduler speculative-decoding torch.compile verified Run pre-commit for new contributors without triggering other tests

Projects

Status: Done
Status: Done
Status: Backlog

Development

Successfully merging this pull request may close these issues.

6 participants