Files
Niko1221andSuperDistance 5d6bf8d414 batch-groups: decode and give way between the pieces of a prompt read (#2050, opt-in STRATA_PIPED_READ_PIECE), on top of #2326's concurrent read
Squash of SuperDistance's three commits of PR #2050 (decode between the pieces of a read on the pipelined groups, the
piece lines counting the rows the slots decoded meanwhile, the stall watchdog staying on for a read in pieces),
applied after #2326 here: the BYIELD give-way is #2326's give_way lambda, the concurrent block runs first and the
piece path serves a read that STRATA_PIPED_CONCURRENT leaves to it (pictures, configurations where it is off).

Co-authored-by: SuperDistance <[email protected]>
2026-10-14 12:26:10 +02:00

88 lines
5.4 KiB
C++

// include/strata/program/batch_read.hpp - how --batch reads a prompt in pieces (header-only: it is arithmetic).
//
// A prompt read with --batch stops between two pieces to (a) let the slots decode and (b) look for a BYIELD (a
// shorter request is waiting). The pieces always end on a multiple of the prompt chunk, so they read exactly the
// chunks a single run would.
#pragma once
#include <algorithm>
#include <cstdint>
namespace strata::program {
/// Tokens of the next piece of a --batch prompt read, or 0 for "the whole rest in one run".
/// beside_slots: some slot is decoding (its windows run between the pieces)
/// layer_split / piped: the read goes through a layer split's stages / the pipelined groups
/// chunks_per_piece: for a layer split with no slot decoding, the pieces' length in chunks (0: one run)
/// A layer split reads its stages as a pipeline over one run's chunks, so a piece costs a pipeline fill; with slots
/// decoding the pieces are single chunks anyway. With no slot decoding a piece is `chunks_per_piece` chunks: long
/// enough to keep the pipeline full, short enough that a BYIELD is taken within a few chunks (it used to wait for the
/// whole prompt, and the long prompt then held its slot: "ERR BGEN: no such free slot" for the request that should
/// have taken over).
inline int64_t batch_read_piece(bool batch, bool piped, bool layer_split, bool beside_slots, int64_t chunk,
int64_t chunks_per_piece) {
if (!batch || piped) return 0;
chunk = std::max<int64_t>(chunk, 1);
if (!layer_split || beside_slots) return chunk;
return chunks_per_piece > 0 ? chunk * chunks_per_piece : 0;
}
/// What a configuration has that keeps the concurrent read (STRATA_PIPED_CONCURRENT=1) off.
struct ConcurrentReadSetup {
bool prefill_help = false; ///< STRATA_PREFILL_HELP: the idle stage's help allocates on the read's thread
bool split_mtp_batch = false; ///< STRATA_SPLIT_MTP_BATCH: the batched draft K/V frees there (a free waits)
bool fs_slots = false; ///< STRATA_FS_SLOTS (Foresight swap space): serving a doorbell makes a CUDA call
bool pcie_dma = false; ///< --pcie-mode dma: the same (its copies)
bool helper_caches = false; ///< --expert-cache-device1..3 (and --remote-expert-opt on them), a prompt source of
///< its own; --remote-expert-opt alone is inert on a plain layer split
bool staged_source = false; ///< the GGUF source stages its blobs (in place, unbuffered, STRATA_IO_PREFETCH):
///< it recycles the buffers it hands out, one thread at a time
bool other_source = false; ///< an expert source that is neither the GGUF source nor the arena
};
/// Why the concurrent read stays off with `s` (the first reason, for the log line), or nullptr when it can run.
inline const char* concurrent_read_off(const ConcurrentReadSetup& s) {
if (s.prefill_help) return "STRATA_PREFILL_HELP";
if (s.split_mtp_batch) return "STRATA_SPLIT_MTP_BATCH";
if (s.fs_slots) return "STRATA_FS_SLOTS";
if (s.pcie_dma) return "--pcie-mode dma";
if (s.helper_caches) return "the helper caches (--expert-cache-device1..3)";
if (s.staged_source)
return "an expert source that stages its blobs (the GGUF in place, unbuffered, STRATA_IO_PREFETCH)";
if (s.other_source) return "this expert source";
return nullptr;
}
/// Tokens of the next piece of a prompt read on the pipelined slot groups (--batch-groups > 1 on a layer split, #1906),
/// or 0 for "the whole rest in one run" (the opt-in is off).
/// piece_tokens: STRATA_PIPED_READ_PIECE, rounded down to whole chunks (at least one, at most kPipedMaxChunks)
/// A piece is one pipelined run over its chunks, so the stages stay overlapped inside it and only its end drains the
/// pipeline. At a piece's end the decoding slots decode, and a waiting shorter request can take over (BYIELD), as
/// between the pieces of a read off the pipeline.
inline constexpr int64_t kPipedMaxChunks = 32;
inline int64_t piped_read_piece(int64_t piece_tokens, int64_t chunk) {
if (piece_tokens <= 0) return 0;
chunk = std::max<int64_t>(chunk, 1);
return std::clamp<int64_t>(piece_tokens / chunk, 1, kPipedMaxChunks) * chunk;
}
/// The slots' decode time between two pieces: share x the piece's time (STRATA_BATCH_DECODE_SHARE, the rule the
/// one-GPU path uses between chunks), at most cap_ms (STRATA_PIPED_DECODE_MAX_MS; <= 0: no cap), at least floor_ms
/// (STRATA_PIPED_DECODE_MIN_MS).
inline double piped_decode_budget_ms(double piece_ms, double share, double cap_ms, double floor_ms) {
double b = std::max(0.0, share) * std::max(0.0, piece_ms);
if (cap_ms > 0.0) b = std::min(b, cap_ms);
return std::max(b, std::max(0.0, floor_ms));
}
/// Prompts arriving back to back: a piecewise read that starts within kPipedLeadInWindowMs + budget_ms of the
/// previous piecewise read's end first gives the slots one decode budget (otherwise they stop for both reads).
/// ms_since_prev_end: < 0 when there was no piecewise read yet
inline constexpr double kPipedLeadInWindowMs = 1000.0;
inline bool piped_read_lead_in(bool enabled, bool beside_slots, double ms_since_prev_end, double budget_ms) {
return enabled && beside_slots && ms_since_prev_end >= 0.0 &&
ms_since_prev_end <= kPipedLeadInWindowMs + std::max(0.0, budget_ms);
}
} // namespace strata::program