Skip to content

Pipelined batch: two requests run at half speed (pad rows in group windows, unbalanced slot choice) - #1249

Closed
crazyaimachine wants to merge 2 commits into
Niko1221:mainfrom
crazyaimachine:pipeline-groups
Closed

crazyaimachine wants to merge 2 commits into
Niko1221:mainfrom
crazyaimachine:pipeline-groups

Conversation

@crazyaimachine

@crazyaimachine crazyaimachine commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Two small fixes for --batch N --batch-groups G on a layer split. Two concurrent requests ran at about half the speed of the 4-request case per request, sometimes worse.

1. A group window holds only the slots up to its last active one (generate.cpp)

Every pipelined group window ran all GS slots of its group. An idle slot ran a pad row through every layer of every stage. The server spreads requests over the groups first (slot_order). So with --batch 4 --batch-groups 2 and two requests, every window was half pad rows. Now PGroup::S is the group's last active slot + 1, and the window, its picks and batch_launch() cover only those slots.

This fix was in the #559 series (ca74f92 there, "pipelined group windows only up to the last active slot"). It did not come along into 0.1.39, where the non-pipelined path got the same idea.

2. pick_slot() spreads requests over the groups (serve/server.py)

When no slot held the prompt's start, pick_slot() took the least recently used free slot. Once every slot held an old conversation, two new requests could land in one group while the other group's stage sat idle. Now the rule is:

  1. A held prefix of at least 512 tokens still wins, because reading a long conversation again costs more.
  2. Otherwise the free slot in the group with the fewest busy slots is taken.
  3. Then an empty slot.
  4. Then the least recently used one.

Measured

Setup:

  • 2× TITAN RTX (Turing, PCIe 3.0), UD-Q4_K_XL with the BF16 PLE table.
  • --batch 4 --batch-groups 2 --trim-stage-weights, layer split 22, images on.
  • Through the HTTP server, T = 0.7, 400 tokens per answer, total tok/s.
Concurrent 0.1.40.1 with these two commits
1 48–58 47–59
2 36–38 (each request ~19 tok/s) 52–82
4 118–122 117–123

Without --batch-groups (one group), 0.1.40.1 gave 50 / 68 / 94 on the same machine.

Not solved

Even with both fixes, a round of two requests now and then still runs at ~19 tok/s each, with the requests in slots 0 and 2 (different groups). In six back-to-back rounds the worst was 52 tok/s total, but it does happen. I have not found the cause yet. If you have an idea where to look (doorbells? the stage hand-off?), I am happy to test.

The two commits are on 0.1.40.1 and apply to current main. Written by my agent, measured on my machine.

crazyaimachine and others added 2 commits October 6, 2026 22:05
…ctive one

With --batch-groups, every group window ran all GS slots, and an idle slot ran a
pad row through every layer of every stage. The server spreads requests over the
groups first, so with --batch 4 --batch-groups 2 and two requests each window was
half pad rows. Now PGroup::S is the group's last active slot + 1, and the window,
its picks and batch_launch() cover only those slots.

This was in the PR Niko1221#559 series (ca74f92 there) and did not come along into 0.1.39.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s worth keeping

pick_slot() took the least recently used free slot when no slot held the
prompt's start. With --batch-groups that could put two requests into one group
while the other group's stage sat idle. A held prefix of at least 512 tokens
still wins (reading a conversation again costs more); otherwise the free slot in
the group with the fewest busy slots is taken, then an empty one, then the least
recently used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blange48

blange48 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

This overlaps with #1179 (opened a few hours earlier, the replacement of #793 after the history rewrite), so here are notes to line the two up.

Commit 1 (group window up to the last active slot) is the same change as #1179's first commit (your 8119a7f / ca74f92 from #559). One of the two PRs can carry it.

Commit 2 (slot choice) solves the same problem with a different rule. #1179's pick_slot (from @rhgo1749's report on #793) works like this with pipelined groups (the engine reports batch_groups= on its INFO line):

  1. it fills a free slot below a busy slot of the same group first (repairs [_ B] after a departure);
  2. then it follows slot_order (which already spreads over groups).

It ignores held prefixes there, because a pipelined slot is never kept as a conversation cache (BDONE clears cached), so the 512-token prefix rule in your step 1 may never fire on a pipelined split. Worth checking on your side. serve/test_slot_alloc.py has 2,000 random arrivals/departures asserting no pick leaves a free slot below it in its group.

Measured on 4× RTX 5080, --batch 8 --batch-groups 4 --trim-stage-weights, IQ3_S, T=0.7, 300 tokens, total tok/s, same image without/with the allocator part: 1: 120.6 → 122.9, 2: 149.1 → 150.6, 3: 141.8 → 212.1, 4: 222.2 → 251.3, 8: 372 → 372.

On your "not solved" case (two requests in slots 0 and 2, still ~19 tok/s each): I have not reproduced it on 4 groups of 2. One thing to rule out is a stale [_ B] shape in one group left by an earlier round, which rule 1 above repairs and an LRU / fewest-busy rule does not. Logging the slot index of each admission plus the window sizes (PGroup::S) per round would show it. Happy to run your repro on our 4-GPU box if you post the client script.

Niko1221 added a commit that referenced this pull request Oct 7, 2026
…e group, and idle slots no longer cost rows in a group's window (#1249)

On a layer split with --batch-groups, two concurrent requests ran at about half speed: the slot choice could put both in
one group, and a group's window carried a row for every slot, active or not. Slot choice now prefers the group with the
fewest busy slots (a held prefix of 512 tokens or more still wins), and a window covers the slots up to its last active
one. Without --batch-groups the slot choice is exactly as before.

Co-authored-by: crazyaimachine <209005491+crazyaimachine@users.noreply.github.com>
@Niko1221

Niko1221 commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Thank you, crazyaimachine. The batch-group fixes are in 0.1.40.2 (Co-authored-by credit). One change: the slot choice and the 512-token prefix threshold apply only with --batch-groups, so the default slot choice is exactly as before.

@Niko1221 Niko1221 closed this Oct 7, 2026
fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 7, 2026
Upstream now carries our picks of Niko1221#851 (AVX2 Q8_K quantizer), Niko1221#789 (expert uploads beside the shared expert),
Niko1221#813 (--ple-io ram on Windows), Niko1221#1185 (window graphs free older batch layouts), Niko1221#1201/Niko1221#1118 (doorbell per
window), Niko1221#1101/Niko1221#1057 (Stager sleep: now the default on Linux only, STRATA_STAGER_SLEEP=1 forces it) and Niko1221#1063
(the shared draft head's type); their versions are kept.

Conflicts resolved:
- prefill: upstream's own CPU share (STRATA_PREFILL_CPU_SHARE, opt-in) sits beside CPU assist.  Its setter is
  set_cpu_share_pool; when it is asked for, it replaces CPU assist for the chunks it covers, otherwise CPU
  assist runs as before (on by default).  The debug NaN check skips both kinds of CPU rows.
- conversation cache: a pool-borrowed entry and upstream's pinned shared prefix (pin=N) are both kept from
  eviction; the borrow (Niko1221#1164) and pin=N's sibling path do not park at once.
- generate: pool batch groups launch only up to their last active slot (Niko1221#1249's G.S), also on the workers;
  --adapt-async's poll between verdicts runs beside the pool's verified-path-first loop.
- server: upstream's chunked bodies (Niko1221#893) and the pool's read-once body are one helper; do_POST's BadBody
  wrapper around the pool's routed-request check.
- native dense: the pool's served_bytes beside upstream's weight_bytes_for (Niko1221#1238).
- expert source: the commit-limit retry (Niko1221#1064) beside upstream's paced page-locking (Niko1221#1250).
- setup: no ready-made engine (StrataPool compiles its own); REPO names StrataPool for the SHA-256 lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
webzone added a commit to webzone/strata-gfx906 that referenced this pull request Oct 7, 2026
Merged 23 upstream commits, from the previous merge base e8ca9af (v0.1.40.2) to
upstream tag v0.1.40.3 (d5ea713). Upstream did not
rewrite history: e8ca9af is still an ancestor of upstream/main. The llama.cpp/ggml
pin is unchanged at 3cf03257f219afbe7334045ff7c6a06ac68c627d.

One file conflicted (1 hunk). The resolution keeps this fork's gfx906 paths:
- setup.py: the AMD GPU listing keeps the fork's two-argument
  amd_problem(g, a.experimental_gfx906) calls; upstream's new Linux
  amd_device_access_problem() /dev/kfd warning block is adopted below them.

CMakeLists.txt, src/program/generate.cpp and sycl/setup_intel.py auto-merged and were
verified line-by-line afterwards: all fork lines (STRATA_EXPERIMENTAL_GFX906, the
gfx906 deprecation warning, STRATA_HIP_MULTIGPU_TESTS, the vmm.cpp STRATA_USE_HIP=1
source property, hip_layer_handoff, hip_wave64_probe, the gfx906 oracle tests, the
frozen PGroup::rows window, the mapped-host handoff bridge, the fail-closed model pin)
are present, and the diff of the four shared files against upstream/main contains only
fork gfx906 content. The fork's frozen active-slot mapping (74583c6) stays in every
hunk; upstream's Niko1221#1249 pad-row PGroup::S variant did not return. sycl/setup_intel.py's
stubs still match the merged signatures (amd_problem(g, experimental_gfx906=False),
hip_vision(asked, *, experimental_gfx906=False, archs=())); the new
amd_device_access_problem() call site is safe there because it returns None without
/dev/kfd. The Niko1221#1357 router guard (n_expert == 512 && K == 10) keeps the native router,
which reads exactly 512 floats per row, for 512-expert models such as the acceptance
model GSQ-RCO IQ2_XS, and falls back to the generic router_top10 otherwise; the gfx906
build compiles the same native_router.cu through the compat layer. The new /dev/kfd
permission message in generate.cpp uses ::access/F_OK/R_OK|W_OK via <unistd.h>, already
included in its !defined(_WIN32) branch. The STRATA_MMVQ_IL opt-in change was reverted
upstream before release: zero occurrences on both sides of the merge.

GPU-independent tests on this Mac: py_compile of setup.py, sycl/setup_intel.py and
tools/hip/dll_closure.py passes. tools 'test_setup_*.py' discovery: 412 tests, 2
failures, 2 errors, 1 skipped; full tools 'test_*.py' discovery: 582 tests, 2 failures,
2 errors, 2 skipped; tests.test_gfx906_upstream plus tests.test_gfx906_setup: 29 tests
OK, 5 skipped (the CmakeArchitectureGate class needs cmake, absent on this Mac; no GPU
test was skipped, the modules are CPU-only by design). Upstream d5ea713 alone, in a
throwaway /tmp worktree (removed afterwards), runs the same 'test_setup_*.py' discovery
with 412 tests, 2 failures and 4 errors: every merged-tree failure is also a baseline
failure, so the merge adds no test regression; the merged tree additionally passes the
two HipVision tests that error on pure upstream (upstream's rocm_root fails on a
missing compiler before the test's cmake stub; the fork's wheel-fallback path
tolerates it). tests/test_gfx906_upstream.py now pins source_version() == 0.1.40.3 and
MIN_ENGINE == (0, 1, 40, 3); the remaining 0.1.40.2 literals in tools/ are version
parsing test data, a "supported since" table string and a historical fixture identity,
none of which assert the current source version.

README.md and docs/GFX906.md now state source version v0.1.40.3 / d5ea713; no workload
numbers were moved or relabelled (no new workload observation in this range). The
running T5810 service was not touched: this merge was prepared and tested on the
maintainer's Mac only.
0xrjman pushed a commit to 0xrjman/Strata that referenced this pull request Oct 7, 2026
…e group, and idle slots no longer cost rows in a group's window (Niko1221#1249)

On a layer split with --batch-groups, two concurrent requests ran at about half speed: the slot choice could put both in
one group, and a group's window carried a row for every slot, active or not. Slot choice now prefers the group with the
fewest busy slots (a held prefix of 512 tokens or more still wins), and a window covers the slots up to its last active
one. Without --batch-groups the slot choice is exactly as before.

Co-authored-by: crazyaimachine <209005491+crazyaimachine@users.noreply.github.com>
MistVVK added a commit to MistVVK/XeStrata that referenced this pull request Oct 8, 2026
…1#1249)

スロットの選び方(使用中の少ないグループを先に)は serve 側で、upstream のマージで入っている。
2 枚の GPU での実行は複数 GPU の群でまとめて確かめる。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants