Repository navigation
fix(sampling): treat top_k above the vocabulary as no top-k in the C++ chain #2247
Description
Problem / Background
The stock sampling chain's top_k_filter (src/lib/mlxcel-core/cpp/mlx_cxx_bridge.cpp:6082-6097, origin/main 1b4e3657) calls mlx::core::argpartition(neg_x, top_k - 1, -1) without comparing top_k to the vocabulary size. Found while reviewing PR #2218. This is from reading the code; it has not been reproduced yet.
Current Behavior
argpartitionthrowsstd::invalid_argument("[argpartition] Received invalid kth ...")whenkth >= shape(axis)(src/lib/mlx-cpp/patches-rocm/mlx/ops.cpp:2873-2878, same check upstream). Sotop_k > vocabthrows while the graph is being built.top_k == vocabis valid and keeps every token.fused_sample,fused_sample_xtc,fused_sample_categoricalandfused_sample_probsare notResultbridge functions (src/lib/mlxcel-core/src/lib.rs:2503-2509,:2536,:2557), so the exception reaches cxx's noexcept boundary and callsstd::terminate: the process aborts.- The default GPU path reaches it. A sampled request with only top-k active (
temperature > 0,top_k > vocab,top_p1.0) skips Gumbel (top_k > 0).sampling_rejection_routes(:5858-5871) returns false when top-p is inactive, sofused_sample_impl(:6644-6762) falls through tofused_sample_argpartition_chain. XTC-active configs,fused_sample_categorical,MLXCEL_SAMPLING_REJECTION=0, the CPU device and backends without a rejection port take the chain unconditionally. With top-p active, top-k at or above vocab counts as inactive (:5869) and routes to the kernel, which is why this rarely shows. - Rust does not validate first. The server passes the request's
top_kthrough unchanged (src/server/request_options.rs:293), andfused_sample_dispatch(src/lib/mlxcel-core/src/sampling.rs:560-584) forwardsconfig.top_k. The Rust-side filters already guard (sampling.rs:1235-1238,:1280-1284,:1557-1561applytop_k_filteronly whentop_k < vocab). The C++ chain is the only unguarded call site.
Proposed Solution
In fused_sample_filter_logits, define use_top_k as top_k > 0 && top_k < x.shape(-1). That is the same "top-k at or above vocab is inactive" rule sampling_rejection_routes uses, and llama.cpp treats top_k >= n_vocab as no filtering. Leave the Rust reference top_k_filter (sampling.rs:2518) unchanged, since its callers guard, but add a one-line doc comment that k must be below the vocabulary. Request validation is not needed: an out-of-range top_k is a valid "no top-k" request.
Acceptance Criteria
- (PR fix(sampling): treat top_k above the vocabulary as no top-k in the chain #2254, in
src/lib/mlxcel-core/src/sampling_top_k_vocab_tests.rsto keepsampling.rsfrom growing) A test insrc/lib/mlxcel-core/src/sampling.rsrunsffi::fused_sample,ffi::fused_sample_categoricalandffi::fused_sample_probson a[1, 64]row withtop_k = 65andtop_k = 1_000_000(temperature 0.7, top-p 1.0). None aborts, and the reported distribution equals thetop_k = 0distribution within 1e-6. - (PR fix(sampling): treat top_k above the vocabulary as no top-k in the chain #2254) The same test with XTC active through
fused_sample_xtc. - (PR fix(sampling): treat top_k above the vocabulary as no top-k in the chain #2254, also a server-level
tests/sampling_top_k_above_vocab_e2e.rs) Before the fix, the new test aborts the test binary (state this in the PR).
Verification
cargo test -p mlxcel-core sampling -- --test-threads=1
make verifyRelated: PR #2218, #901 (rejection routing), #1379 (chain order).
Activity
inureyes commentedon Oct 8, 2026
Reproduced on gfx1151 (ROCm, origin/main 1b4e365,
mlxcel-server -m models/mlx/Qwen3-0.6B-4bit). One request aborts the whole server process (exit status 134, SIGABRT), dropping every in-flight request:top_p: 1.0top_p: 0.9,MLXCEL_SAMPLING_REJECTION=0top_p: 0.9ortop_pomitted (model default 0.95)mlxcel generate --temp 0.7 --top-k 1000000mlxcel generate ... --top-k 1000000 --top-p 0.9Raising to
priority:high: any client can take the server down with one field.fix(sampling): treat top_k above the vocabulary as no top-k (lablup#2254