Summary
The audit in #7637 found one production spawn_cpu call site that violates the
"never wait inside spawn_cpu" rule: the streaming/sequential partition search
in IvfSubIndex::search_partitions.
rust/lance/src/index/vector/ivf/v2.rs (streaming branch, ~L1671): a single
spawn_cpu closure pumps a blocking_recv on prepared_rx and blocking_send
on a capacity-1 batch_tx. Both park the CPU-pool thread. The producer feeding
prepared_rx does object-store I/O (and its partition decode can itself need the
CPU pool), so on hosts whose CPU pool collapses to a single blocking thread
(<= 3 CPUs) — or under enough concurrent queries — the parked search thread and
the work that would unblock it can starve each other. This is the same deadlock
class as #7423.
Why it wasn't fixed in the audit PR
This is a deliberate optimization from #6475, not an accident. The sequential
path intentionally runs a query's entire partition search on one CPU worker to
avoid per-partition CPU-task fan-out (measured 14–30% latency improvement under
concurrency), and test_sequential_initial_search_prepares_all_then_searches_on_one_cpu_thread
locks in the "reuse one cpu thread" invariant.
The obvious fix (one spawn_cpu per partition) removes the deadlock risk but
reintroduces the fan-out #6475 avoided and breaks that test — so it trades a
benchmarked perf win against correctness on small hosts. That decision needs input
from the #6475 author rather than a drive-by change.
Options considered
- Per-partition
spawn_cpu: async recv/send loop, one spawn_cpu per
partition. Safe, simple; reintroduces fan-out, needs the "one cpu thread" test
updated. Mild regression on the sequential early-stop path.
spawn_blocking off the CPU pool: keep the single-thread pumping model but
on tokio's large blocking pool instead of the tiny CPU pool. Preserves the
one-thread shape and avoids CPU-pool deadlock, but CPU-bound search then runs
outside the CPU reservation (core oversubscription) and the lance-cpu
thread-name assertion changes.
The test mock PreparedThreadCapturingIndex in rust/lance/src/io/exec/knn.rs
(~L2646) mirrors the same blocking-in-spawn_cpu structure and would need to move
in lockstep with whichever option is chosen.
Acceptance
search_partitions never parks a CPU-pool thread on a channel.
- The chosen approach's perf impact on the sequential path is characterized.
- Regression test covering the streaming path on a single-thread CPU pool (mirror
the #7423 approach).
Follow-up to #7637.
Summary
The audit in #7637 found one production
spawn_cpucall site that violates the"never wait inside
spawn_cpu" rule: the streaming/sequential partition searchin
IvfSubIndex::search_partitions.rust/lance/src/index/vector/ivf/v2.rs(streaming branch, ~L1671): a singlespawn_cpuclosure pumps ablocking_recvonprepared_rxandblocking_sendon a capacity-1
batch_tx. Both park the CPU-pool thread. The producer feedingprepared_rxdoes object-store I/O (and its partition decode can itself need theCPU pool), so on hosts whose CPU pool collapses to a single blocking thread
(
<= 3CPUs) — or under enough concurrent queries — the parked search thread andthe work that would unblock it can starve each other. This is the same deadlock
class as #7423.
Why it wasn't fixed in the audit PR
This is a deliberate optimization from #6475, not an accident. The sequential
path intentionally runs a query's entire partition search on one CPU worker to
avoid per-partition CPU-task fan-out (measured 14–30% latency improvement under
concurrency), and
test_sequential_initial_search_prepares_all_then_searches_on_one_cpu_threadlocks in the "reuse one cpu thread" invariant.
The obvious fix (one
spawn_cpuper partition) removes the deadlock risk butreintroduces the fan-out #6475 avoided and breaks that test — so it trades a
benchmarked perf win against correctness on small hosts. That decision needs input
from the #6475 author rather than a drive-by change.
Options considered
spawn_cpu: async recv/send loop, onespawn_cpuperpartition. Safe, simple; reintroduces fan-out, needs the "one cpu thread" test
updated. Mild regression on the sequential early-stop path.
spawn_blockingoff the CPU pool: keep the single-thread pumping model buton tokio's large blocking pool instead of the tiny CPU pool. Preserves the
one-thread shape and avoids CPU-pool deadlock, but CPU-bound search then runs
outside the CPU reservation (core oversubscription) and the
lance-cputhread-name assertion changes.
The test mock
PreparedThreadCapturingIndexinrust/lance/src/io/exec/knn.rs(~L2646) mirrors the same blocking-in-
spawn_cpustructure and would need to movein lockstep with whichever option is chosen.
Acceptance
search_partitionsnever parks a CPU-pool thread on a channel.the #7423 approach).
Follow-up to #7637.