Files
Strata/tests/program/adaptive_range_test.cpp
40849df0a8 Transfer completion: flag B stays behind queued copies, and the adaptive-copy range covers the whole round (P1.1)
Correctness (class E): kept even without a speedup.

Flag B (PR #5096): the graph waits for "flag B >= layer + 1" and flag B is one counter that only rises. A non-empty
plan raises it from a host function on the copy stream, after its uploads. An empty plan (n <= 0) raised it on the
spot, even when the copy stream still held an earlier layer's, or the other token group's, uploads: the counter
jumped past a layer whose bytes had not landed. Verifier::raise_flag_b_ordered() now raises it through the copy
stream whenever copies were queued in this window, and directly only when none were. The same ordering is used at
the four places that publish an empty plan when the pool did not (run, run_slot_rows, batch_serve, service), which
had the same hazard. A failed host-function launch used to be ignored (flag B never rose and the window hung); it
now stops the engine like a failed upload does, after releasing the GPU waits.

Adaptive-copy bookkeeping (PR #4844): a round planned on the pipelined loop whose loop drained before step 3 is
marked resident on the serial rule, which used the lo/hi/bi of an earlier round, so only that range of swaps was
marked and the others were lost (the slot held the new expert, the table still named the old one). The pipelined plan
now names the whole round. The range arithmetic moved into AdaptiveCopyRange (adaptive_range.hpp) so a host test can
walk the rounds.

Tests: tests/core/flag_order_test.cu (delayed host-function copy standing in for a slow DMA, a consumer kernel that
reads staging once flag B allows it, a negative control that shows the old direct raise releases it early, flag only
rises, nothing queued, graph replay twice, every device; passes on both A4000s of z440), tests/program/
adaptive_range_test.cpp (serial batches for 0, 1, many swaps and several caps, the stale-range loss, empty round).

Co-authored-by: AlanJager <[email protected]>
Co-authored-by: uvlad23 <[email protected]>
2026-11-05 23:39:06 +01:00

103 lines
4.5 KiB
C++

// The adaptive tier's copy-in range (PR #4844): which swaps of a round step 3 marks resident. No GPU: the rounds are
// walked as generate.cpp walks them, and each swap is a counter that says whether it was copied in and whether it was marked.
#include <cstdio>
#include <vector>
#include "strata/program/adaptive_range.hpp"
using strata::program::AdaptiveCopyRange;
static int failed = 0;
#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); ++failed; } } while (0)
struct Round {
size_t total;
std::vector<int> copied, marked;
explicit Round(size_t n) : total(n), copied(n, 0), marked(n, 0) {}
static bool all(const std::vector<int>& v) { for (int x : v) if (x != 1) return false; return true; }
};
// the serial loop: plan (clear + next_batch), copy the batch, step 3 marks it, repeat while more() (the arena's cap)
static void serial_round(AdaptiveCopyRange& r, Round& rd, size_t batch) {
r.clear();
r.next_batch(rd.total, batch);
for (;;) {
for (size_t i = r.lo; i < r.hi; ++i) ++rd.copied[i];
for (size_t i = r.landed_begin(false); i < r.landed_end(false, rd.total); ++i) ++rd.marked[i];
if (!r.more(rd.total)) break;
r.next_batch(rd.total, batch);
}
}
int main() {
// the serial loop covers every swap once, whatever the cap, including zero swaps and one swap
for (size_t total : {(size_t) 0, (size_t) 1, (size_t) 2, (size_t) 5, (size_t) 8, (size_t) 64})
for (size_t batch : {(size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 7, (size_t) 8, (size_t) 100}) {
AdaptiveCopyRange r;
Round rd(total);
serial_round(r, rd, batch);
CHECK(Round::all(rd.copied) && Round::all(rd.marked));
CHECK(r.bi == total && r.hi == total);
}
// a batch never runs past the list, even for a cap near the size_t limit
{
AdaptiveCopyRange r;
r.next_batch(5, (size_t) -1);
CHECK(r.lo == 0 && r.hi == 5 && r.bi == 5);
r.next_batch(5, 2); // asked again after the end: an empty batch at the end
CHECK(r.lo == 5 && r.hi == 5 && !r.more(5));
}
// the lost experts of #4844: a round planned on the pipelined loop whose loop drained before step 3 is marked on the
// serial rule. An earlier serial round left [lo, hi) at its last batch.
{
AdaptiveCopyRange r;
Round first(5);
serial_round(r, first, 2); // leaves lo = 4, hi = 5, bi = 5
CHECK(r.lo == 4 && r.hi == 5);
// the old plan (nothing set for a pipelined round): step 3 on the serial rule marks only the stale range
{
AdaptiveCopyRange stale = r;
Round pl(7);
for (size_t i = 0; i < pl.total; ++i) ++pl.copied[i]; // a_in copied all of them at the gaps
for (size_t i = stale.landed_begin(false); i < stale.landed_end(false, pl.total); ++i) ++pl.marked[i];
CHECK(!Round::all(pl.marked)); // the bug this test pins
}
// the fix: the pipelined plan names the whole round
Round pl(7);
r.whole_round(pl.total);
for (size_t i = 0; i < pl.total; ++i) ++pl.copied[i];
for (size_t i = r.landed_begin(false); i < r.landed_end(false, pl.total); ++i) ++pl.marked[i]; // the drained tick: serial rule
CHECK(Round::all(pl.marked));
CHECK(!r.more(pl.total)); // and no second batch is posted
// the same round finished on the pipelined rule marks the same range
CHECK(r.landed_begin(true) == r.landed_begin(false) && r.landed_end(true, pl.total) == r.landed_end(false, pl.total));
}
// one swap, and an empty round, on the pipelined plan
{
AdaptiveCopyRange r;
r.next_batch(9, 4);
r.whole_round(1);
CHECK(r.landed_begin(false) == 0 && r.landed_end(false, 1) == 1 && !r.more(1));
r.whole_round(0);
CHECK(r.landed_begin(false) == 0 && r.landed_end(false, 0) == 0 && !r.more(0));
}
// a serial round that follows a pipelined one starts clean
{
AdaptiveCopyRange r;
r.whole_round(6);
Round rd(3);
serial_round(r, rd, 2);
CHECK(Round::all(rd.copied) && Round::all(rd.marked));
}
if (failed) { std::fprintf(stderr, "%d check(s) failed\n", failed); return 1; }
std::puts("adaptive_range_test: ok");
return 0;
}