mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
Layer split spill walk: stage-weight trim for short cards, drafter drop and head on host per card, KV window and fewer-cards steps; launcher retry without a drafter and the engine's own reason as the last line
This commit is contained in:
1 parent
55a2bcd474
commit
fc5dac23c5
21 files changed
+595
-44
No files matched your search
@@ -388,6 +388,11 @@ if(STRATA_BUILD_TESTS)
|
||||
target_include_directories(stage_reserve_test PRIVATE include)
|
||||
add_test(NAME stage_reserve_test COMMAND stage_reserve_test)
|
||||
|
||||
# the layer split's spill walk (header-only arithmetic: who is short, which step comes next)
|
||||
add_executable(split_walk_test tests/program/split_walk_test.cpp)
|
||||
target_include_directories(split_walk_test PRIVATE include)
|
||||
add_test(NAME split_walk_test COMMAND split_walk_test)
|
||||
|
||||
# the prompt planner's host-memory model (#2824): header-only, no GPU
|
||||
add_executable(prefill_plan_test tests/program/prefill_plan_test.cpp)
|
||||
target_include_directories(prefill_plan_test PRIVATE include)
|
||||
|
||||
@@ -193,6 +193,15 @@ add("The all-cards split engine could not start, so the normal layer split runs.
|
||||
"O motor de divisão entre todas as placas não conseguiu iniciar, então a divisão normal por camadas está em uso.",
|
||||
"모든 카드를 쓰는 분할 엔진을 시작하지 못해 일반 레이어 분할로 실행합니다.",
|
||||
"Le moteur réparti sur toutes les cartes n'a pas pu démarrer, la répartition normale par couches est donc utilisée.")
|
||||
add("Drafting: off (the cards were short of memory)",
|
||||
"预测草稿:关闭(显卡内存不足)",
|
||||
"先読み:オフ(グラフィックスカードのメモリが不足していました)",
|
||||
"Vorausraten: aus (den Karten fehlte Speicher)",
|
||||
"Predicción: desactivada (a las tarjetas les faltaba memoria)",
|
||||
"Предсказание: выключено (на картах не хватило памяти)",
|
||||
"Previsão: desligada (faltou memória nas placas)",
|
||||
"초안 예측: 꺼짐(카드 메모리가 부족했습니다)",
|
||||
"Anticipation : désactivée (les cartes manquaient de mémoire)")
|
||||
here = Path(__file__).resolve().parent.parent / "src" / "locales"
|
||||
NBSP = " "
|
||||
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "Der MTP-Entwurfsbaustein konnte nicht heruntergeladen werden.",
|
||||
"df17": "Vorausraten: MTP (DFlash2 ist nicht gestartet)",
|
||||
"df18": "Automatisch nutzt die Alle-Karten-Split-Engine mit der MTP-Entwurfsschicht: etwa {0} GB, einmalig herunterzuladen.",
|
||||
"df19": "Die Alle-Karten-Split-Engine konnte nicht starten, deshalb läuft die normale Schichtaufteilung."
|
||||
"df19": "Die Alle-Karten-Split-Engine konnte nicht starten, deshalb läuft die normale Schichtaufteilung.",
|
||||
"df20": "Vorausraten: aus (den Karten fehlte Speicher)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "The MTP drafter could not be downloaded.",
|
||||
"df17": "Drafting: MTP (DFlash2 didn't start)",
|
||||
"df18": "Auto uses the all-cards split engine with the MTP draft layer: about {0} GB to download, once.",
|
||||
"df19": "The all-cards split engine could not start, so the normal layer split runs."
|
||||
"df19": "The all-cards split engine could not start, so the normal layer split runs.",
|
||||
"df20": "Drafting: off (the cards were short of memory)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "No se pudo descargar el predictor de MTP.",
|
||||
"df17": "Predicción: MTP (DFlash2 no arrancó)",
|
||||
"df18": "Automático usa el motor de reparto entre todas las tarjetas con la capa de borrador MTP: unos {0} GB, una sola vez.",
|
||||
"df19": "El motor de reparto entre todas las tarjetas no pudo arrancar, así que se usa el reparto normal por capas."
|
||||
"df19": "El motor de reparto entre todas las tarjetas no pudo arrancar, así que se usa el reparto normal por capas.",
|
||||
"df20": "Predicción: desactivada (a las tarjetas les faltaba memoria)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "Le module d’anticipation MTP n’a pas pu être téléchargé.",
|
||||
"df17": "Anticipation : MTP (DFlash2 n’a pas démarré)",
|
||||
"df18": "Auto utilise le moteur réparti sur toutes les cartes avec la couche de brouillon MTP : environ {0} Go à télécharger, une seule fois.",
|
||||
"df19": "Le moteur réparti sur toutes les cartes n’a pas pu démarrer, la répartition normale par couches est donc utilisée."
|
||||
"df19": "Le moteur réparti sur toutes les cartes n’a pas pu démarrer, la répartition normale par couches est donc utilisée.",
|
||||
"df20": "Anticipation : désactivée (les cartes manquaient de mémoire)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "MTP のドラフトモデルをダウンロードできませんでした。",
|
||||
"df17": "先読み:MTP(DFlash2 は起動しませんでした)",
|
||||
"df18": "自動では、全カードのスプリットエンジンと MTP ドラフト層を使います。約 {0} GB を一度だけダウンロードします。",
|
||||
"df19": "全カードのスプリットエンジンを起動できなかったため、通常のレイヤー分割で動作しています。"
|
||||
"df19": "全カードのスプリットエンジンを起動できなかったため、通常のレイヤー分割で動作しています。",
|
||||
"df20": "先読み:オフ(グラフィックスカードのメモリが不足していました)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "MTP 초안 모델을 내려받지 못했습니다.",
|
||||
"df17": "초안 예측: MTP(DFlash2가 시작되지 않았습니다)",
|
||||
"df18": "자동은 모든 카드를 쓰는 분할 엔진과 MTP 초안 층을 사용합니다(약 {0}GB, 한 번만 내려받음).",
|
||||
"df19": "모든 카드를 쓰는 분할 엔진을 시작하지 못해 일반 레이어 분할로 실행합니다."
|
||||
"df19": "모든 카드를 쓰는 분할 엔진을 시작하지 못해 일반 레이어 분할로 실행합니다.",
|
||||
"df20": "초안 예측: 꺼짐(카드 메모리가 부족했습니다)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "Não foi possível baixar o preditor do MTP.",
|
||||
"df17": "Previsão: MTP (o DFlash2 não iniciou)",
|
||||
"df18": "O automático usa o motor de divisão entre todas as placas com a camada de rascunho MTP: cerca de {0} GB, uma vez.",
|
||||
"df19": "O motor de divisão entre todas as placas não conseguiu iniciar, então a divisão normal por camadas está em uso."
|
||||
"df19": "O motor de divisão entre todas as placas não conseguiu iniciar, então a divisão normal por camadas está em uso.",
|
||||
"df20": "Previsão: desligada (faltou memória nas placas)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "Не удалось скачать черновую модель MTP.",
|
||||
"df17": "Предсказание: MTP (DFlash2 не запустился)",
|
||||
"df18": "Авто использует движок с разделением по всем картам и черновой слой MTP: около {0} ГБ, скачивается один раз.",
|
||||
"df19": "Движок с разделением по всем картам не запустился, поэтому работает обычное послойное разделение."
|
||||
"df19": "Движок с разделением по всем картам не запустился, поэтому работает обычное послойное разделение.",
|
||||
"df20": "Предсказание: выключено (на картах не хватило памяти)"
|
||||
}
|
||||
@@ -17,5 +17,6 @@
|
||||
"df16": "无法下载 MTP 草稿模型。",
|
||||
"df17": "预测草稿:MTP(DFlash2 未能启动)",
|
||||
"df18": "自动模式使用全部显卡的拆分引擎和 MTP 草稿层:约 {0} GB,只下载一次。",
|
||||
"df19": "全显卡拆分引擎未能启动,因此改用常规的分层拆分。"
|
||||
"df19": "全显卡拆分引擎未能启动,因此改用常规的分层拆分。",
|
||||
"df20": "预测草稿:关闭(显卡内存不足)"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// include/strata/program/split_walk.hpp - the spill walk of a layer split: what each card gives up, in which order, when it is short of VRAM.
|
||||
//
|
||||
// The 0.2.10 walk (draft layer dropped, a loan that keeps 16 slots, the prompt path's own small buffers, the output head in pinned host memory, a smaller KV window)
|
||||
// ran for ONE card. A layer split went straight to "expert cache: no room" or "the GPU has too little free VRAM for the prompt path" (two 8 GB cards, 128K: 6 of 6
|
||||
// starts failed). The arithmetic of the layer split's version is here, pure, so a host test pins it (tests/program/split_walk_test.cpp); src/program/generate.cpp
|
||||
// reads the cards and prints the lines.
|
||||
//
|
||||
// The steps, mildest first, each only when the one before did not leave the card enough:
|
||||
// 1. trim: every card loads only the dense weights of ITS layers (--trim-stage-weights). Without it each card holds the whole 3.5 GiB set (IQ3_S). Bit-exact in
|
||||
// itself; the cache simply gets the room. This is the step that matters on 8 GB cards.
|
||||
// 2. the KV window down (32768 -> 20480 -> 8192 -> 4096 cells a layer; the rest streams from RAM), when no placement can start the prompt path.
|
||||
// 3. the drafter dropped on the card that holds it (draft layer or DFlash2's pack and buffers): decode is slower, the start works.
|
||||
// 4. the loan keeps 16 slots, and the prompt path's own small buffers (256..1024-token chunks), instead of 128 and 1024..8192.
|
||||
// 5. the output head in pinned host memory, on the card that holds it.
|
||||
// 6. fewer cards: a card with too little free memory to hold a share is left out of the split; one card left is the single-card start (with its own walk).
|
||||
// The context and the number of requests are never touched. A big card never reaches any of them: trim_needed() is false when the card has the room.
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::program::split_walk {
|
||||
|
||||
constexpr int64_t kContextMib = 1550; ///< CUDA context, streams, scratch and tables of a card that holds the whole dense set (8 GB hog, IQ3_S: 1559 MiB measured)
|
||||
constexpr int64_t kWorkingCacheSlots = 263; ///< a cache the engine calls working (generate.cpp #496)
|
||||
constexpr int64_t kPromptHeadroomMib = 1100; ///< a 512-token chunk's own buffers (570 MiB) + the serve check's 512 MiB headroom
|
||||
constexpr int64_t kMinCardMib = 4608; ///< under this a card cannot hold even a trimmed share beside the context, a working cache and the prompt buffers
|
||||
constexpr int64_t kStageLateAllocMib = 256; ///< what a stage allocates after its cache that nothing prices (the single card's kLateAllocMib, #4474)
|
||||
|
||||
/// What one card needs, from the model and the start (all MiB).
|
||||
struct Model {
|
||||
int64_t dense_mib = 0; ///< the whole dense set a card loads without the trim (pack arena + native projections)
|
||||
int64_t session_mib = 0; ///< the session of one card's layers at the asked context and KV window
|
||||
int64_t head_mib = 0; ///< the output head (the last stage only)
|
||||
int64_t drafter_mib = 0; ///< the draft layer or DFlash2 (the stage that carries it only)
|
||||
int64_t slot_mib = 3; ///< the largest expert blob, rounded up
|
||||
int64_t reserve_mib = 796; ///< what the plan keeps free on a later stage (700 + the verify windows)
|
||||
};
|
||||
|
||||
/// MiB a card needs beyond its free memory figure when it loads the whole dense set: the weights, the context, its session, (the last stage) the head and the drafter,
|
||||
/// the reserve, a working cache and the prompt path's headroom.
|
||||
inline int64_t untrimmed_need_mib(const Model& m, bool carries_head_and_drafter) {
|
||||
return m.dense_mib + kContextMib + m.session_mib + (carries_head_and_drafter ? m.head_mib + m.drafter_mib : 0) + m.reserve_mib +
|
||||
kWorkingCacheSlots * m.slot_mib + kPromptHeadroomMib;
|
||||
}
|
||||
|
||||
/// Is this card too short to start without the trim? (free_mib: its free memory before it loaded anything.)
|
||||
inline bool trim_needed(int64_t free_mib, const Model& m, bool carries_head_and_drafter) {
|
||||
return free_mib > 0 && free_mib < untrimmed_need_mib(m, carries_head_and_drafter);
|
||||
}
|
||||
|
||||
/// May this card take part in a layer split at all?
|
||||
inline bool card_usable(int64_t free_mib) { return free_mib >= kMinCardMib; }
|
||||
|
||||
/// The single card's rule for dropping the draft layer, unchanged (generate.cpp, "SHORT OF VRAM: THE DRAFT LAYER IS THE LAST THING TO GO"): the layer, the head, a working
|
||||
/// cache beside the prompt buffers (214 slots, 543 MiB) and what is allocated after it. Used for the card that holds the drafter in a layer split too.
|
||||
inline uint64_t draft_layer_need_bytes(uint64_t layer_bytes, uint64_t head_bytes) {
|
||||
return layer_bytes + head_bytes + (220ull << 20) + (1550ull << 20);
|
||||
}
|
||||
inline bool draft_layer_short(uint64_t free_bytes, uint64_t layer_bytes, uint64_t head_bytes) {
|
||||
return free_bytes > 0 && free_bytes < draft_layer_need_bytes(layer_bytes, head_bytes);
|
||||
}
|
||||
|
||||
/// DFlash2 on a stage: its pack and its K/V ring and buffers (0.25 GiB), its draft head, and the same working cache and late allocations.
|
||||
inline uint64_t dflash2_need_bytes(uint64_t pack_bytes, uint64_t head_bytes) {
|
||||
return pack_bytes + (250ull << 20) + head_bytes + (220ull << 20) + (1550ull << 20);
|
||||
}
|
||||
|
||||
/// The next KV window down from `current` (0: the whole context in VRAM) for a context of `ctx` cells, or 0 when it is already the smallest (`min_cells`).
|
||||
inline int64_t next_kv_window(int64_t current, int64_t ctx, int64_t min_cells) {
|
||||
constexpr int64_t steps[] = {20480, 8192, 4096};
|
||||
const int64_t cur = current > 0 ? current : ctx;
|
||||
for (const int64_t s : steps)
|
||||
if (s < cur && s < ctx && s >= min_cells) return s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// The layer share of each of `n` cards, proportional to what each has free beyond `fixed_mib`; at least 2 layers a card, summing to `layers`. Used when the split is
|
||||
/// chosen before any card has loaded (trimmed on both), so the weights of CUDA0 can be trimmed too.
|
||||
inline void proportional_layers(const int64_t* free_mib, int n, int64_t layers, int64_t fixed_mib, int64_t* out) {
|
||||
int64_t total = 0;
|
||||
for (int i = 0; i < n; ++i) total += std::max<int64_t>(free_mib[i] - fixed_mib, 1);
|
||||
int64_t given = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const int64_t w = std::max<int64_t>(free_mib[i] - fixed_mib, 1);
|
||||
out[i] = i + 1 < n ? std::max<int64_t>(2, (layers * w + total / 2) / total) : 0;
|
||||
given += out[i];
|
||||
}
|
||||
out[n - 1] = layers - given;
|
||||
}
|
||||
|
||||
} // namespace strata::program::split_walk
|
||||
+39
-8
@@ -1087,8 +1087,10 @@ class StrataEngine:
|
||||
|
||||
def __init__(self, exe: str, args: list[str], cwd: str | None = None, log: str | None = None,
|
||||
env: dict | None = None, lazy: bool = False, cpus: list[int] | None = None, _attempt: int = 0,
|
||||
retry_args: list[str] | None = None):
|
||||
retry_args: list[str] | None = None, retry_vram_args: list[str] | None = None):
|
||||
# retry_args: the arguments to start again with, once, when this start ends before the engine is ready (an Auto start that picked DFlash2: mtp_retry_args)
|
||||
# retry_vram_args: the same start with no drafter (nodraft_retry_args), used instead when the engine ended because a card ran out of memory: swapping DFlash2 for
|
||||
# the MTP takes MORE memory from a card that is already short, so that direction is never taken then
|
||||
self.spawn = (exe, list(args), cwd, log, env, False, cpus) # to start it again after it died (issue #27)
|
||||
# the all-cards split engine (sFNX, strata-split-N[-arch]): its GEN parser accepts fewer keys than Strata's engine (tests/split/test_split_gen_keys.py pins them)
|
||||
self.split_engine = os.path.basename(str(exe)).startswith("strata-split")
|
||||
@@ -1214,9 +1216,22 @@ class StrataEngine:
|
||||
f"ready yet: starting it again in {wait_s:.0f} s (try {_attempt + 2} of {START_RETRIES + 1})",
|
||||
flush=True)
|
||||
time.sleep(wait_s)
|
||||
StrataEngine.__init__(self, exe, args, cwd, log, env, lazy, cpus, _attempt + 1, retry_args)
|
||||
StrataEngine.__init__(self, exe, args, cwd, log, env, lazy, cpus, _attempt + 1, retry_args, retry_vram_args)
|
||||
return
|
||||
if not timed_out and retry_args and retry_args != list(args):
|
||||
short_reason = None if timed_out else vram_fit.engine_stop_reason(start_log_text(log, log_start).splitlines())
|
||||
if short_reason and retry_vram_args and retry_vram_args != list(args):
|
||||
# a card ran out of memory (the engine's own reason is in its log): one more start with no drafter, in one quiet line
|
||||
print(f"[strata] {drafting.line('off', drafting.SHORT_OF_VRAM)}: starting again without a drafter ({short_reason})", flush=True)
|
||||
try:
|
||||
with open(log, "a", encoding="utf-8") as handle:
|
||||
handle.write(f"[strata] {time.strftime('%Y-%m-%d %H:%M:%S')} the engine ended before it was ready because a card was short of memory "
|
||||
f"(exit code {self.proc.returncode}); starting it again without a drafter\n")
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
drafting.remember_note(drafting.SHORT_OF_VRAM)
|
||||
StrataEngine.__init__(self, exe, retry_vram_args, cwd, log, env, lazy, cpus, 0, None, None)
|
||||
return
|
||||
if not timed_out and not short_reason and retry_args and retry_args != list(args):
|
||||
# Auto picked DFlash2 and the engine ended before it was ready (an engine that does not take one of its flags, a drafter file it cannot read...):
|
||||
# one more start with the MTP, said in one quiet line. The person named DFlash2 themselves: no retry, the start fails with its reason below.
|
||||
print(f"[strata] {drafting.line('mtp', drafting.DIDNT_START)}: starting again with the MTP", flush=True)
|
||||
@@ -1227,7 +1242,7 @@ class StrataEngine:
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
drafting.remember_note(drafting.DIDNT_START)
|
||||
StrataEngine.__init__(self, exe, retry_args, cwd, log, env, lazy, cpus, 0, None)
|
||||
StrataEngine.__init__(self, exe, retry_args, cwd, log, env, lazy, cpus, 0, None, None)
|
||||
return
|
||||
why = (f"the engine did not report READY within {ENGINE_READY_S:.0f} s" if timed_out else
|
||||
"the engine exited before it was ready")
|
||||
@@ -3489,6 +3504,22 @@ def mtp_retry_args(cfg: dict, exe: str | None, extra: list[str] | None = None) -
|
||||
return alt if alt != first + list(extra or []) else None
|
||||
|
||||
|
||||
def nodraft_retry_args(cfg: dict, exe: str | None, extra: list[str] | None = None) -> list[str] | None:
|
||||
"""The arguments to start again with when an Auto start ends before the engine is ready because a card ran out of memory: the same start with no drafter
|
||||
(the draft layer and DFlash2's buffers are the biggest optional part on a card that is short), never the MTP in place of DFlash2. None when the drafting
|
||||
was not Auto (the person named a drafter: that start fails with the engine's reason), when the first start has no drafter anyway, or when nothing differs."""
|
||||
if drafting.normalize(cfg.get("drafting")) != "auto" or "args" not in cfg:
|
||||
return None
|
||||
try:
|
||||
first = engine_args(cfg, exe, announce=False)
|
||||
if "--draft" not in first and "--mtp" not in first:
|
||||
return None
|
||||
alt = engine_args({**cfg, "drafting": "off"}, exe, announce=False) + list(extra or [])
|
||||
except Exception: # noqa: BLE001 - a retry that cannot be worked out is no retry
|
||||
return None
|
||||
return alt if alt != first + list(extra or []) else None
|
||||
|
||||
|
||||
def engine_args(cfg: dict, exe: str | None = None, *, announce: bool = True) -> list[str]:
|
||||
"""The engine's arguments: the config's, and with several GPUs the layer split across them ("layer_split" in the
|
||||
config: "auto" by default, or the first layer of each later GPU's share, e.g. "18" or "16,32"; see
|
||||
@@ -9112,7 +9143,7 @@ def start_engine_or_fallback(cfg: dict, exe: str, effort_end: list[str] | None,
|
||||
fb = cfg.get("split_fallback")
|
||||
try:
|
||||
return StrataEngine(exe, engine_args(cfg, exe) + (effort_end or []), cwd=cfg.get("cwd"), log=cfg.get("log"), env=env, lazy=lazy,
|
||||
retry_args=mtp_retry_args(cfg, exe, effort_end))
|
||||
retry_args=mtp_retry_args(cfg, exe, effort_end), retry_vram_args=nodraft_retry_args(cfg, exe, effort_end))
|
||||
except Exception as e:
|
||||
if not isinstance(fb, dict) or not fb.get("exe"):
|
||||
raise
|
||||
@@ -9140,7 +9171,7 @@ def start_engine_or_fallback(cfg: dict, exe: str, effort_end: list[str] | None,
|
||||
configure_batch_telemetry_env(env2, cfg2, batch_telemetry)
|
||||
effort2 = effort_end_args(cfg2, exe2, tok)
|
||||
engine = StrataEngine(exe2, engine_args(cfg2, exe2) + (effort2 or []), cwd=cfg2.get("cwd"), log=cfg2.get("log"), env=env2, lazy=lazy,
|
||||
retry_args=mtp_retry_args(cfg2, exe2, effort2))
|
||||
retry_args=mtp_retry_args(cfg2, exe2, effort2), retry_vram_args=nodraft_retry_args(cfg2, exe2, effort2))
|
||||
engine.info["split_fallback"] = True
|
||||
return engine
|
||||
|
||||
@@ -9168,7 +9199,7 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
|
||||
try:
|
||||
engines[i] = StrataEngine(exe, engine_args(rc, exe) + effort_end, cwd=rc.get("cwd"), log=rc.get("log"),
|
||||
env=child_env(rc, offline=offline), lazy=lazy, cpus=cpus[i] if cpus else None,
|
||||
retry_args=mtp_retry_args(rc, exe, effort_end))
|
||||
retry_args=mtp_retry_args(rc, exe, effort_end), retry_vram_args=nodraft_retry_args(rc, exe, effort_end))
|
||||
except BaseException as e: # noqa: BLE001
|
||||
errors.append((i, e))
|
||||
rc0 = rp.replica_cfg(cfg, 0, groups[0])
|
||||
@@ -9235,7 +9266,7 @@ def start_split(cfg: dict, spec: dict, exe: str, effort_end: list[str], lazy: bo
|
||||
if role == "prefill":
|
||||
warn_budget_over_ram(engine_args(rc, announce=False) if "args" in rc else [])
|
||||
engines[role] = StrataEngine(exe, engine_args(rc, exe) + effort_end, cwd=rc.get("cwd"), log=rc.get("log"),
|
||||
env=child_env(rc, offline=offline), lazy=lazy, retry_args=mtp_retry_args(rc, exe, effort_end))
|
||||
env=child_env(rc, offline=offline), lazy=lazy, retry_args=mtp_retry_args(rc, exe, effort_end), retry_vram_args=nodraft_retry_args(rc, exe, effort_end))
|
||||
except BaseException:
|
||||
for e in engines.values():
|
||||
try:
|
||||
|
||||
@@ -19,6 +19,11 @@ if '--draft' in args and '--die-on-draft' in args:
|
||||
sys.stderr.write('strata generate: --draft dflash2 drafts without the MTP layer and needs --draft-model PACK, --spec 2 or more and --mtp DIR (for DIR/draft_vocab.bin)\n')
|
||||
sys.stderr.flush()
|
||||
raise SystemExit(2)
|
||||
if '--die-short' in args and ('--draft' in args or '--mtp' in args):
|
||||
sys.stderr.write('ggml_cuda_init: failed to initialize CUDA: no CUDA-capable device is detected'+chr(10))
|
||||
sys.stderr.write('strata generate: layer split, CUDA1 expert cache: no room'+chr(10))
|
||||
sys.stderr.flush()
|
||||
raise SystemExit(1)
|
||||
if '--die-always' in args:
|
||||
sys.stderr.write('strata generate: something else is wrong\n')
|
||||
sys.stderr.flush()
|
||||
@@ -93,6 +98,33 @@ class AutoRetriesWithTheMtp(unittest.TestCase):
|
||||
self.assertEqual(len(self.started()), 1)
|
||||
self.assertEqual(engine.info.get("drafting_note", ""), "")
|
||||
|
||||
def test_a_start_that_ran_out_of_memory_is_repeated_with_no_drafter_never_with_the_mtp(self):
|
||||
"""2 small cards on a layer split: the engine says a card has no room. The old retry swapped DFlash2 for the MTP, which takes MORE memory from a card that is short."""
|
||||
first = [a for a in self.first if a != "--die-on-draft"] + ["--die-short"]
|
||||
mtp = [a for a in self.mtp if a != "--die-on-draft"] + ["--die-short"]
|
||||
nodraft = ["--pack", "/d/packs/g", "--starts", str(self.starts), "--die-short"]
|
||||
engine = StrataEngine(str(self.script), first, log=self.log, retry_args=mtp, retry_vram_args=nodraft)
|
||||
self.addCleanup(engine.close)
|
||||
self.assertTrue(engine.alive())
|
||||
starts = self.started()
|
||||
self.assertEqual(len(starts), 2, "one start with DFlash2, one without a drafter, never the MTP in between")
|
||||
self.assertIn("--draft dflash2", starts[0])
|
||||
self.assertNotIn("--mtp", starts[1])
|
||||
self.assertNotIn("--draft", starts[1])
|
||||
self.assertEqual(engine.spawn[1], nodraft)
|
||||
self.assertEqual(engine.info["drafting"], "off")
|
||||
self.assertEqual(engine.info["drafting_note"], "Drafting: off (the cards were short of memory)")
|
||||
text = Path(self.log).read_text(encoding="utf-8")
|
||||
self.assertIn("short of memory", text)
|
||||
self.assertIn("expert cache: no room", text, "the engine's own reason stays in the log")
|
||||
|
||||
def test_a_memory_stop_without_a_no_drafter_retry_fails_with_the_engines_reason_and_does_not_try_the_mtp(self):
|
||||
first = [a for a in self.first if a != "--die-on-draft"] + ["--die-short"]
|
||||
with self.assertRaises(EngineDied) as caught:
|
||||
StrataEngine(str(self.script), first, log=self.log, retry_args=[*self.mtp, "--die-short"])
|
||||
self.assertIn("expert cache: no room", str(caught.exception))
|
||||
self.assertEqual(len(self.started()), 1)
|
||||
|
||||
def test_the_same_arguments_are_not_a_retry(self):
|
||||
with self.assertRaises(EngineDied):
|
||||
StrataEngine(str(self.script), self.first, log=self.log, retry_args=list(self.first))
|
||||
|
||||
+173
-22
@@ -99,6 +99,7 @@
|
||||
#include "strata/program/conv_cache.hpp"
|
||||
#include "strata/program/split_rules.hpp"
|
||||
#include "strata/program/stage_reserve.hpp"
|
||||
#include "strata/program/split_walk.hpp"
|
||||
#include "strata/program/gpu_report.hpp"
|
||||
#include "strata/prefill/share_rules.hpp"
|
||||
#include "strata/platform/host_limits.hpp"
|
||||
@@ -3962,6 +3963,27 @@ int main(int argc, char** argv) {
|
||||
"(--split-device 0); disable parking with --conversation-cache-mib 0\n");
|
||||
return 2;
|
||||
}
|
||||
// THE LAYER SPLIT'S SPILL WALK, STEP 6: FEWER CARDS. Under `auto`, a later card with too little free memory to hold even a trimmed share (a card another program fills) is left out
|
||||
// of the split, said plainly; with none left the start is the single card's, with its own walk and the rest of the experts in RAM. Only the cards the engine would have picked.
|
||||
if (ok && split_auto && !split_devs.empty() && std::getenv("STRATA_SPILL_OFF") == nullptr && spill_discrete_gpu()) {
|
||||
std::vector<int> kept;
|
||||
for (const int d : split_devs) {
|
||||
size_t fb_c = 0, tb_c = 0;
|
||||
bool usable = true;
|
||||
{
|
||||
const strata::core::OnDevice on_c(d);
|
||||
usable = cudaMemGetInfo(&fb_c, &tb_c) != cudaSuccess || strata::program::split_walk::card_usable((int64_t) (fb_c >> 20));
|
||||
(void) cudaGetLastError();
|
||||
}
|
||||
if (usable) { kept.push_back(d); continue; }
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: CUDA%d has only %.2f GiB free, too little to hold a share of the model beside its context, a working cache "
|
||||
"and the prompt buffers (%.1f GiB at least): the layer split runs without it\n",
|
||||
d, (double) fb_c / 1073741824.0, (double) strata::program::split_walk::kMinCardMib / 1024.0);
|
||||
}
|
||||
if (kept.empty())
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: no other card has room, so the model runs on CUDA0 alone with the experts that do not fit in RAM\n");
|
||||
split_devs = kept;
|
||||
}
|
||||
if (ok && split_auto && split_devs.empty()) {
|
||||
std::fprintf(stderr, "strata generate: --layer-split auto: one GPU visible, so no split\n");
|
||||
o.layer_split.clear();
|
||||
@@ -4859,7 +4881,10 @@ int main(int argc, char** argv) {
|
||||
// Only `--layer-split auto` WITH the trim asked loads the later stages' weights after the split search (they can
|
||||
// only be trimmed once the split is known). Every other run keeps the 0.1.39 order: weights first, then the
|
||||
// search - so without the trim nothing about a split moves.
|
||||
const bool late_weights = trim_asked && multi_gpu && split_auto;
|
||||
bool late_weights = trim_asked && multi_gpu && split_auto;
|
||||
// THE LAYER SPLIT'S SPILL WALK, STEP 1 (include/strata/program/split_walk.hpp): a card too short for the whole dense set loads only its own layers' weights. Decided below,
|
||||
// once the dense set's size is known and before any later stage loads; `walk_trim` is that decision, `later_trim` the stages' trim (asked for, or the walk's).
|
||||
bool walk_trim = false;
|
||||
// keep_routers: CUDA0 with --mmap-experts keeps every layer's router (ffn_gate_inp, ~2.5 MiB a layer) - the file
|
||||
// tier's routing-aware prefetch (RouterLookahead, below) copies all of them from CUDA0's arena, and would
|
||||
// otherwise turn itself off
|
||||
@@ -4968,6 +4993,40 @@ int main(int argc, char** argv) {
|
||||
std::fprintf(stderr, "strata generate: %zu native projection matrices, %.2f MiB of weights, in %.1f s\n",
|
||||
native_dense.tensor_count(), (double) native_dense.weight_bytes() / (1024.0 * 1024.0), load_s());
|
||||
}
|
||||
// ---- THE LAYER SPLIT'S SPILL WALK, STEP 1: THE TRIM. CUDA0 holds the whole dense set now; every later card is about to load a copy of it (3.5 GiB for IQ3_S), which on a
|
||||
// card that is short (two 8 GB cards: 6 of 6 starts failed in 0.2.10) is what leaves its expert cache and its prompt path no room. A card that cannot afford the copy
|
||||
// beside its session, its head, its drafter and a working cache loads only the dense weights of its own layers instead (--trim-stage-weights, bit-exact in itself:
|
||||
// the cache just gets the room). A card with room never reaches this; STRATA_STAGE_TRIM=0 or STRATA_SPILL_OFF=1 keeps the old copy. CUDA0 stays whole under
|
||||
// `--layer-split auto` (its weights load before the split is known); it runs the card's share of layers the search gives it.
|
||||
if (multi_gpu && !trim_asked && !split_devs.empty() && std::getenv("STRATA_SPILL_OFF") == nullptr &&
|
||||
!(std::getenv("STRATA_STAGE_TRIM") != nullptr && std::string(std::getenv("STRATA_STAGE_TRIM")) == "0") && spill_discrete_gpu()) {
|
||||
strata::program::split_walk::Model wm;
|
||||
wm.dense_mib = (int64_t) ((pool_bytes + (o.native_dense_gguf.empty() ? 0 : native_dense.weight_bytes())) >> 20);
|
||||
const int ns_w = (int) split_devs.size() + 1;
|
||||
wm.session_mib = (int64_t) (strata::core::session_bytes(g, o.max_context, K, 0, (g.n_layers + ns_w - 1) / ns_w) >> 20);
|
||||
if (const strata::core::WeightRef* wo_w = wt.find("output.weight"); wo_w != nullptr && !o.native_head_gguf.empty())
|
||||
wm.head_mib = (int64_t) (((uint64_t) wo_w->ne1 * (uint64_t) (g.n_embd / 256) * 176ull) >> 20); // Q5_K: 176 bytes per 256 values
|
||||
wm.drafter_mib = (!o.mtp.empty() || o.draft == "dflash2") ? strata::program::stage_reserve::kDrafterMib : 0;
|
||||
wm.slot_mib = std::max<int64_t>(1, (int64_t) ((strata::kernels::cpu::expert_layout().max_blob + (1u << 20) - 1) >> 20));
|
||||
wm.reserve_mib = (o.vram_reserve_later_mib >= 0 ? o.vram_reserve_later_mib : o.vram_reserve_mib) + strata::program::stage_reserve::kWindowMib;
|
||||
for (size_t i = 0; i < split_devs.size(); ++i) {
|
||||
const strata::core::OnDevice on_w(split_devs[i]);
|
||||
size_t fb_w = 0, tb_w = 0;
|
||||
if (cudaMemGetInfo(&fb_w, &tb_w) != cudaSuccess) { (void) cudaGetLastError(); continue; }
|
||||
const bool last_w = i + 1 == split_devs.size();
|
||||
if (strata::program::split_walk::trim_needed((int64_t) (fb_w >> 20), wm, last_w)) {
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: CUDA%d has %.2f GiB free and a layer split card needs about %.2f GiB beside a full copy of the dense weights "
|
||||
"(%.2f GiB): each later card loads only the dense weights of its own layers (STRATA_STAGE_TRIM=0 keeps the copy)\n",
|
||||
split_devs[i], (double) fb_w / 1073741824.0, (double) strata::program::split_walk::untrimmed_need_mib(wm, last_w) / 1024.0,
|
||||
(double) wm.dense_mib / 1024.0);
|
||||
walk_trim = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (walk_trim && split_auto) late_weights = true;
|
||||
}
|
||||
// the stages' dense weights are trimmed to their own layers: asked for (an explicit split), or by the walk; under `auto` they load after the split search (late_weights)
|
||||
const bool later_trim = stage_trim || (walk_trim && !split_auto && !split_at.empty());
|
||||
|
||||
strata::kernels::gr_set_fp32_activations(o.gr_fp32_activations);
|
||||
strata::kernels::gr_set_native_mmvf(o.gr_native_mmvf);
|
||||
@@ -5529,9 +5588,9 @@ int main(int argc, char** argv) {
|
||||
// Later stages' weights load here (0.1.39), except `--layer-split auto` with the trim asked: those load after
|
||||
// the split search (see late_weights), so `auto` can trim them too.
|
||||
if (!late_weights) {
|
||||
const int64_t lb = stage_trim ? split_at[i] : 0;
|
||||
const int64_t le = stage_trim ? (i + 1 < split_at.size() ? split_at[i + 1] : g.n_layers) : 0;
|
||||
if (!load_stage_weights(st, lb, le, stage_trim)) return 1;
|
||||
const int64_t lb = later_trim ? split_at[i] : 0;
|
||||
const int64_t le = later_trim ? (i + 1 < split_at.size() ? split_at[i + 1] : g.n_layers) : 0;
|
||||
if (!load_stage_weights(st, lb, le, later_trim)) return 1;
|
||||
}
|
||||
// THE SESSION AND THE HEAD WAIT FOR THE SPLIT SEARCH. `session_bytes` prices a stage's session by its
|
||||
// LAYER RANGE (the carve - every stage used to hold all 48 layers' state whatever it ran), so the
|
||||
@@ -5748,8 +5807,11 @@ int main(int argc, char** argv) {
|
||||
std::fprintf(stderr, "strata generate: layer split: CUDA%d free memory: %s\n", dev < 0 ? 0 : dev,
|
||||
cudaGetErrorString(e));
|
||||
const int64_t pf = search && split_own_auto && !place_with_reserve ? 0 : split_pf_mib;
|
||||
// what a later stage allocates after its cache and nothing prices (cuBLAS, the modules and the MMQ pool the prompt path warms up, ~250 MiB measured): the single card prices
|
||||
// it (kLateAllocMib, #4474); a stage did not, and two 16 GB cards ended a start with 131 MiB free and ran out of memory in the first long prompt. Not under STRATA_DEFAULTS=026.
|
||||
const int64_t late_alloc = later && !strata::qwen_defaults_026() ? strata::program::split_walk::kStageLateAllocMib : 0;
|
||||
const int64_t base_reserve =
|
||||
later && o.vram_reserve_later_mib >= 0 ? o.vram_reserve_later_mib : o.vram_reserve_mib;
|
||||
(later && o.vram_reserve_later_mib >= 0 ? o.vram_reserve_later_mib : o.vram_reserve_mib) + late_alloc;
|
||||
strata::program::stage_reserve::Inputs rin;
|
||||
rin.base_reserve_mib = base_reserve;
|
||||
rin.prompt_buffers_mib = pf;
|
||||
@@ -6261,6 +6323,27 @@ int main(int argc, char** argv) {
|
||||
std::fprintf(stderr, "strata generate: to make room: a smaller --max-context, --prefill 512, a smaller "
|
||||
"--vram-reserve-mib, an explicit --layer-split, or close other programs using the GPU\n");
|
||||
};
|
||||
// THE LAYER SPLIT'S SPILL WALK, STEP 2: THE KV WINDOW DOWN. No placement can start the prompt path with the window asked for: the sessions of the cards are smaller with a
|
||||
// smaller one (the rest of the K/V streams from pinned RAM), so the search runs again at 20480, 8192, 4096 cells a layer (the engine's smallest) and keeps the first window
|
||||
// that has a startable placement. The context and the number of requests are not touched. Never reached by a start that has a placement.
|
||||
if (best.empty() && std::getenv("STRATA_SPILL_OFF") == nullptr && spill_discrete_gpu()) {
|
||||
const int64_t kv_asked = o.kv_resident;
|
||||
for (int64_t w; (w = strata::program::split_walk::next_kv_window(o.kv_resident, o.max_context, strata::core::qsa_kv_resident_min())) > 0;) {
|
||||
o.kv_resident = w;
|
||||
strata::core::qsa_set_kv_resident(w);
|
||||
search();
|
||||
if (!best.empty()) {
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: the KV window is %lld cells a layer in VRAM instead of %s, the rest is read from RAM: the context "
|
||||
"(%lld tokens) is unchanged\n", (long long) w,
|
||||
kv_asked > 0 ? std::to_string((long long) kv_asked).c_str() : "the whole context", (long long) o.max_context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (best.empty()) {
|
||||
o.kv_resident = kv_asked;
|
||||
strata::core::qsa_set_kv_resident(kv_asked);
|
||||
}
|
||||
}
|
||||
if (best.empty()) {
|
||||
// recommend, never force: with nothing startable on paper the search runs again without the gate, so the
|
||||
// run is as it was before the gate, and what is short is said
|
||||
@@ -6752,20 +6835,54 @@ int main(int argc, char** argv) {
|
||||
// the sessions) the start used to stop at "no VRAM is left for the expert cache" or run out of memory in the head upload. Without the layer
|
||||
// the model still answers (the verify window drafts from the prompt alone): slower decode, a start that works. Not with DFlash2 (its own
|
||||
// block decides), a split, or STRATA_MTP_KEEP=1.
|
||||
if (!o.mtp.empty() && !use_df2 && !multi_gpu && stages.empty() && std::getenv("STRATA_MTP_KEEP") == nullptr && spill_discrete_gpu()) {
|
||||
std::error_code fec1, fec2;
|
||||
const uint64_t layer_b = (uint64_t) std::filesystem::file_size(std::filesystem::path(o.mtp) / "dense.bin", fec1) +
|
||||
(uint64_t) std::filesystem::file_size(std::filesystem::path(o.mtp) / "experts.bin", fec2);
|
||||
const strata::core::WeightRef* wo_mtp = wt.find("output.weight");
|
||||
const uint64_t head_b = (!o.native_head_gguf.empty() && wo_mtp != nullptr)
|
||||
? (uint64_t) wo_mtp->ne1 * (uint64_t) (g.n_embd / 256) * 176ull : 0; // Q5_K: 176 bytes per 256 values
|
||||
const uint64_t free_now = (uint64_t) strata::core::device_free_bytes();
|
||||
const uint64_t need = layer_b + head_b + (220ull << 20) + (1550ull << 20); // + the cache the loan needs (214 slots, 543 MiB) + what is allocated after it and the drafter beyond DFlash2 (measured: DFlash2 alone ends 391 MiB free, MTP 41 at the same cache)
|
||||
if (!fec1 && !fec2 && free_now > 0 && free_now < need) {
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: %.2f GiB is free, and the draft layer (%.2f GiB), the head (%.2f GiB) and a working cache "
|
||||
"need about %.2f GiB: this start runs without the draft layer (decode is slower; STRATA_MTP_KEEP=1 keeps it)\n",
|
||||
(double) free_now / 1073741824.0, (double) layer_b / 1073741824.0, (double) head_b / 1073741824.0, (double) need / 1073741824.0);
|
||||
o.mtp.clear();
|
||||
// A LAYER SPLIT WALKS THE SAME STEP ON THE CARD THAT HOLDS THE DRAFTER (the last stage's, or CUDA0 with the drafter first): its free memory is read there, after its weights, session
|
||||
// and head are in. DFlash2 (its pack, its ring and buffers, its draft head) is dropped by the same rule on that card; the single card keeps it as before (its own block decides).
|
||||
{
|
||||
const bool split_walk_drafter = multi_gpu && !stages.empty() && std::getenv("STRATA_SPILL_OFF") == nullptr;
|
||||
if (!o.mtp.empty() && !use_df2 && (split_walk_drafter || (!multi_gpu && stages.empty())) && std::getenv("STRATA_MTP_KEEP") == nullptr && spill_discrete_gpu()) {
|
||||
std::error_code fec1, fec2;
|
||||
const uint64_t layer_b = (uint64_t) std::filesystem::file_size(std::filesystem::path(o.mtp) / "dense.bin", fec1) +
|
||||
(uint64_t) std::filesystem::file_size(std::filesystem::path(o.mtp) / "experts.bin", fec2);
|
||||
const strata::core::WeightRef* wo_mtp = wt.find("output.weight");
|
||||
const uint64_t head_b = (!o.native_head_gguf.empty() && wo_mtp != nullptr)
|
||||
? (uint64_t) wo_mtp->ne1 * (uint64_t) (g.n_embd / 256) * 176ull : 0; // Q5_K: 176 bytes per 256 values
|
||||
const int draft_dev = split_walk_drafter ? (last_st && !mtp_first ? last_st->dev : -1) : -1;
|
||||
uint64_t free_now = 0;
|
||||
{
|
||||
const strata::core::OnDevice on_draft(draft_dev);
|
||||
free_now = (uint64_t) strata::core::device_free_bytes();
|
||||
}
|
||||
const uint64_t need = strata::program::split_walk::draft_layer_need_bytes(layer_b, head_b); // + the cache the loan needs (214 slots, 543 MiB) + what is allocated after it and the drafter beyond DFlash2 (measured: DFlash2 alone ends 391 MiB free, MTP 41 at the same cache)
|
||||
if (!fec1 && !fec2 && strata::program::split_walk::draft_layer_short(free_now, layer_b, head_b)) {
|
||||
const std::string card = split_walk_drafter ? "CUDA" + std::to_string(draft_dev < 0 ? 0 : draft_dev) + ": " : std::string();
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: %s%.2f GiB is free, and the draft layer (%.2f GiB), the head (%.2f GiB) and a working cache "
|
||||
"need about %.2f GiB: this start runs without the draft layer (decode is slower; STRATA_MTP_KEEP=1 keeps it)\n",
|
||||
card.c_str(), (double) free_now / 1073741824.0, (double) layer_b / 1073741824.0, (double) head_b / 1073741824.0, (double) need / 1073741824.0);
|
||||
o.mtp.clear();
|
||||
}
|
||||
}
|
||||
// DFlash2 alone (--draft-fallback none, what Auto passes) on a stage that cannot hold it: no drafter, said in one line, instead of a start that ends
|
||||
if (split_walk_drafter && df2_solo && !use_df2 && std::getenv("STRATA_MTP_KEEP") == nullptr && spill_discrete_gpu()) {
|
||||
std::error_code fec3;
|
||||
const uint64_t pack_b = (uint64_t) std::filesystem::file_size(o.draft_model, fec3);
|
||||
const strata::core::WeightRef* wo_df = wt.find("output.weight");
|
||||
const uint64_t head_b = (!o.native_head_gguf.empty() && wo_df != nullptr)
|
||||
? (uint64_t) wo_df->ne1 * (uint64_t) (g.n_embd / 256) * 176ull : 0;
|
||||
const int dev_df = last_st && !df2_first ? last_st->dev : -1;
|
||||
uint64_t free_df = 0;
|
||||
{
|
||||
const strata::core::OnDevice on_df(dev_df);
|
||||
free_df = (uint64_t) strata::core::device_free_bytes();
|
||||
}
|
||||
const uint64_t need_df = strata::program::split_walk::dflash2_need_bytes(pack_b, head_b / 2);
|
||||
if (!fec3 && free_df > 0 && free_df < need_df) {
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: CUDA%d: %.2f GiB is free, and DFlash2 (%.2f GiB with its buffers and draft head) and a working cache need about "
|
||||
"%.2f GiB: this start runs without a drafter (decode is slower; STRATA_MTP_KEEP=1 keeps it)\n",
|
||||
dev_df < 0 ? 0 : dev_df, (double) free_df / 1073741824.0, (double) pack_b / 1073741824.0 + 0.45, (double) need_df / 1073741824.0);
|
||||
df2_solo = false;
|
||||
o.draft.clear();
|
||||
o.draft_model.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!o.mtp.empty() || df2_solo) { // (df2_solo: o.mtp is empty and no MTP layer is loaded; only the DFlash2 block below runs)
|
||||
@@ -7630,7 +7747,7 @@ int main(int argc, char** argv) {
|
||||
// than that (8 GB NVIDIA, 128K: 112-212 slots) used to be squeezed by lowering the reserve to give the cache its 326 working slots, which left 100 MiB
|
||||
// free and stopped the first long prompt ("mmq: out of memory"), or gave the prompt path its own 600 MiB of buffers on top and stopped with "do not
|
||||
// fit". The loan may leave 16 slots instead: the cache is smaller, prompts are read in smaller chunks and slower, the reserve stays, and the start works.
|
||||
if (o.prefill_chunk > 0 && pf_borrow && o.prefill_auto && o.expert_cache < min_slots && !o.vram_reserve_given && !multi_gpu && stages.empty() &&
|
||||
if (o.prefill_chunk > 0 && pf_borrow && o.prefill_auto && o.expert_cache < min_slots && !o.vram_reserve_given &&
|
||||
std::getenv("STRATA_SPILL_OFF") == nullptr && spill_discrete_gpu()) {
|
||||
constexpr int64_t kTightKeep = 16;
|
||||
const int64_t tight_min = ((int64_t) strata::prefill::Prefill::bytes_needed(g, ss, 256) + blob - 1) / blob + kTightKeep;
|
||||
@@ -7677,7 +7794,7 @@ int main(int argc, char** argv) {
|
||||
// verify arena, or DFlash2's staging) stopped with "do not fit" (8 GB NVIDIA, 128K, 2 requests: a 74-slot cache and 35 MiB left). Price them
|
||||
// here instead, at the largest chunk that still leaves a cache (1024, 512, 256 tokens), and give the cache the rest: slower (a smaller
|
||||
// cache, smaller prompt chunks), never a refused start. Not with a split, a cache or chunk given on the command line, or STRATA_SPILL_OFF=1.
|
||||
if (auto_cache && pf_borrow && o.prefill_auto && !o.vram_reserve_given && o.expert_cache < min_slots && !multi_gpu && stages.empty() && !profile.empty() &&
|
||||
if (auto_cache && pf_borrow && o.prefill_auto && !o.vram_reserve_given && o.expert_cache < min_slots && !profile.empty() &&
|
||||
std::getenv("STRATA_SPILL_OFF") == nullptr && spill_discrete_gpu()) {
|
||||
constexpr int64_t kSpillFloorMib = 100; // the verify arena, the head check and the cuBLAS workspace beside the late allocations
|
||||
int64_t pick_chunk = 0, pick_slots = 0, pick_own = 0, pick_late = 0;
|
||||
@@ -8163,8 +8280,25 @@ int main(int argc, char** argv) {
|
||||
const auto& lay = strata::kernels::cpu::expert_layout();
|
||||
// the drafter and the head are already allocated by now (they load above, before this), so what is left
|
||||
// to hold back is the windows - and `free_b` has already lost the drafter.
|
||||
const int64_t room = stage_room(st.dev, true, false);
|
||||
int64_t room = stage_room(st.dev, true, false);
|
||||
const strata::core::OnDevice on(st.dev);
|
||||
// THE LAYER SPLIT'S SPILL WALK, STEP 5: THE OUTPUT HEAD ON THE HOST, LAST. The card that holds the head (the last stage) has no room for a working cache beside it: the head
|
||||
// (0.41 GiB) moves to mapped pinned host memory and every logits row reads it over PCIe (decode is slower, the start works). Only a card that would otherwise refuse.
|
||||
if (&st == last_st && st.head.loaded() && !st.head.on_host() && !o.vram_reserve_given && std::getenv("STRATA_HEAD_HOST_OFF") == nullptr &&
|
||||
std::getenv("STRATA_SPILL_OFF") == nullptr && spill_discrete_gpu() &&
|
||||
room < strata::program::split_walk::kWorkingCacheSlots * (int64_t) lay.max_blob) {
|
||||
std::string herr;
|
||||
const uint64_t head_mib = st.head.weight_bytes() >> 20;
|
||||
if (st.head.to_host(herr)) {
|
||||
room = stage_room(st.dev, true, false);
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: CUDA%d: the cache would have no room, so the output head (%llu MiB) moves to pinned host memory and is read over "
|
||||
"PCIe for every logits row: decode is slower; the room for experts is now %.2f GiB\n",
|
||||
st.dev, (unsigned long long) head_mib, (double) room / 1073741824.0);
|
||||
} else {
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: CUDA%d: the output head stays in VRAM (%s)\n", st.dev,
|
||||
herr.empty() ? "type or build not movable" : herr.c_str());
|
||||
}
|
||||
}
|
||||
{
|
||||
size_t fb = 0, tb = 0;
|
||||
cudaMemGetInfo(&fb, &tb);
|
||||
@@ -8261,6 +8395,23 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (multi_gpu)
|
||||
std::fprintf(stderr, "strata generate: layer split: CUDA0 runs layers 0-%lld\n", (long long) (split_at[0] - 1));
|
||||
// THE LAYER SPLIT'S SPILL WALK, STEP 4: THE LOAN KEEPS 16. A stage whose cache is too small to lend the prompt path a 256-token chunk and keep 128 slots behind makes the path
|
||||
// allocate its own buffers on top (more than the reserve): the same short card as the single card's, which keeps 16 slots instead (the cache is smaller, prompts are read in
|
||||
// smaller chunks, the start works). Never on a card with room: it only fires when a cache is below the usual loan.
|
||||
if (multi_gpu && !stages.empty() && g_lend_keep == 128 && o.prefill_chunk > 0 && pf_borrow && o.prefill_auto && !o.vram_reserve_given &&
|
||||
std::getenv("STRATA_SPILL_OFF") == nullptr && spill_discrete_gpu()) {
|
||||
for (const auto& stp : stages) {
|
||||
const int64_t blob_s = (int64_t) nibble_max_slot_bytes();
|
||||
const int64_t need_s = ((int64_t) strata::prefill::Prefill::bytes_needed(g, stp->ss, 256) + blob_s - 1) / blob_s + g_lend_keep;
|
||||
if ((int64_t) stp->cache.slots() < need_s) {
|
||||
std::fprintf(stderr, "strata generate: short of GPU memory: CUDA%d: the cache (%lld slots) is too small to lend the prompt path 256-token buffers and keep %lld more: "
|
||||
"the loan keeps 16, prompts are read in smaller chunks, expect them to be slower\n",
|
||||
stp->dev, (long long) stp->cache.slots(), (long long) g_lend_keep);
|
||||
g_lend_keep = 16;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nib::any_on()) {
|
||||
// STRATA_EXPERT_NIBBLE: every expert of every layer must be in VRAM, in the cache of the card that runs the layer.
|
||||
// A converted layer's miss would be computed from the GGUF blob by the CPU or over PCIe, and a non-converted
|
||||
|
||||
@@ -22,6 +22,7 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from strata_control import vram_fit
|
||||
from strata_control import atomic as _atomic_mod
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -2685,6 +2686,8 @@ CAUSE_LINE = re.compile(r"error|exception|failed|cannot|can't|could not|no devic
|
||||
HSA_START = re.compile(r"hsa_exception|HSA_STATUS_ERROR", re.I)
|
||||
HSA_START_NEXT = ("The graphics card was not ready yet; wait a few seconds and run it again. If it keeps happening, restart the PC "
|
||||
"and report the log.")
|
||||
VRAM_STOP_NEXT = ("A graphics card is too full for this start, even after Strata moved what it could to the processor and the RAM. Close other programs that use the "
|
||||
"cards, or start on fewer cards (--gpu), or turn pictures off (--vision off); the context and the number of requests were not changed.")
|
||||
KNOWN_CAUSES = (
|
||||
(re.compile(r"cannot run the expert kernel|needs AVX-?512", re.I),
|
||||
"This model folder holds the pack for CPUs with AVX-512, and this PC's CPU does not have it. Run `strata setup` again: on a CPU "
|
||||
@@ -2756,7 +2759,10 @@ def explain_engine_exit(result: dict, config: Path | None, marks: dict | None =
|
||||
if own:
|
||||
lines, log = own, str(server_log)
|
||||
lines = lines or [x.strip() for x in str(result.get("log_tail") or "").split(" | ") if x.strip()]
|
||||
cause = next((x for x in reversed(lines) if CAUSE_LINE.search(x) and not x.lstrip().startswith(("strata generate: ", "strata serve: expert"))), None)
|
||||
# a card that ran out of memory: the engine's own line is the cause (the picture reader's "ggml_cuda_init: ... no CUDA-capable device", which it prints when the
|
||||
# cards are full and it runs on the processor, matched "failed" and used to be the last line shown)
|
||||
vram_cause = vram_fit.engine_stop_reason(lines)
|
||||
cause = vram_cause or next((x for x in reversed(lines) if CAUSE_LINE.search(x) and not x.lstrip().startswith(("strata generate: ", "strata serve: expert"))), None)
|
||||
cause = (cause or (lines[-1] if lines else "")).strip()
|
||||
cause = re.sub(r"^(\[strata\]\s*)?(\d{4}-\d\d-\d\d[ T][\d:.]+\s*)?", "", cause)[:240]
|
||||
state, why = result.get("state"), result.get("error") or result.get("message")
|
||||
@@ -2779,7 +2785,7 @@ def explain_engine_exit(result: dict, config: Path | None, marks: dict | None =
|
||||
text = stage + (f": {cause}" if cause else ".")
|
||||
if not text.endswith((".", "!")):
|
||||
text += "."
|
||||
step = HSA_START_NEXT if hsa else next((words for pattern, words in KNOWN_CAUSES if pattern.search(" ".join([cause, str(why or "")]))), "Run it again; if it repeats, report the log.")
|
||||
step = HSA_START_NEXT if hsa else (VRAM_STOP_NEXT if vram_cause else next((words for pattern, words in KNOWN_CAUSES if pattern.search(" ".join([cause, str(why or "")]))), "Run it again; if it repeats, report the log."))
|
||||
if shared_memory and not hsa and not any(p.search(" ".join([cause, str(why or "")])) for p, _ in KNOWN_CAUSES[:2]):
|
||||
# a Strix Halo: the settings fix the expert cache beside a RAM budget, and the engine ended with no error text of its own (STRIX-2Q, 11-01)
|
||||
try:
|
||||
|
||||
@@ -422,7 +422,8 @@ NO_MTP = "the MTP drafter isn't downloaded yet"
|
||||
NONE_HERE = "no drafter is downloaded yet"
|
||||
DIDNT_START = "DFlash2 didn't start" # Auto picked DFlash2, the engine ended before it was ready, and the start was repeated once with the MTP (serve/server.py)
|
||||
NOT_SERVED = NOT_HERE
|
||||
REASONS = (DIDNT_FIT, NOT_HERE, NOT_SUPPORTED, NOT_DOWNLOADED, NOT_THIS_CARD, NO_MTP, NONE_HERE, DIDNT_START)
|
||||
SHORT_OF_VRAM = "the cards were short of memory" # the engine ended before it was ready for lack of graphics memory: the start was repeated once with no drafter (serve/server.py), never with the MTP
|
||||
REASONS = (DIDNT_FIT, NOT_HERE, NOT_SUPPORTED, NOT_DOWNLOADED, NOT_THIS_CARD, NO_MTP, NONE_HERE, DIDNT_START, SHORT_OF_VRAM)
|
||||
|
||||
|
||||
def line(drafter: str, reason: str = "") -> str:
|
||||
|
||||
@@ -35,6 +35,7 @@ It is an estimate; the engine still says what is short if it is off.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
CHUNK_STEPS = (1024, 512, 256) # a smaller prompt chunk, tried after the default one (1024 is the value measured end to end)
|
||||
@@ -329,3 +330,33 @@ def fit_start(cfg: dict, args: list, vram_mib: float | None, backend: str | None
|
||||
def fit_context(model: str, vram_gb: float, ctx: int, kv: str = "int8", reserve_mib: int = 300, vendor: str = "intel") -> int:
|
||||
"""The context to recommend: always `ctx`. A fit does not change the context any more (owner, 2026-11-02); kept for the callers that ask."""
|
||||
return int(ctx)
|
||||
|
||||
|
||||
# ----- a start that ended because a card ran out of memory --------------------------------------------------------------------------------------------------
|
||||
# The engine's own lines for it (src/program/generate.cpp, serve): the cache has no room, the prompt path or a weight arena does not fit, the card has too
|
||||
# little free VRAM. They start with "strata generate:" / "strata serve:", which the launcher's generic cause search skips (those logs are chatty), and the words
|
||||
# are not the generic "error|failed" ones, so the last such line used to be the picture reader's harmless "ggml_cuda_init: failed to initialize CUDA: no
|
||||
# CUDA-capable device" (it runs on the processor when the cards are full). One list, so the launcher's final line and the server's retry agree.
|
||||
ENGINE_STOP_PATTERNS = (
|
||||
re.compile(r"expert cache: no room", re.I),
|
||||
re.compile(r"no VRAM is left for the expert cache", re.I),
|
||||
re.compile(r"too little free VRAM", re.I),
|
||||
re.compile(r"cannot start the prompt path", re.I),
|
||||
re.compile(r"device buffers .* do not fit", re.I),
|
||||
re.compile(r"weight arena (does not fit|failed)|cudaMalloc\(\d+\) for the weight arena failed", re.I),
|
||||
re.compile(r"(session state|head|native dense projections|weights).*(out of memory|does not fit|failed)", re.I),
|
||||
)
|
||||
ENGINE_LINE = re.compile(r"^(?:\[strata\]\s*)?strata (?:generate|serve|mtp|dflash2|prefill): ", re.I)
|
||||
# a line that only reports what the walk did, never why a start stopped
|
||||
_NOT_A_STOP = re.compile(r"short of GPU memory: |trying a smaller|trying \d+ of \d+ slots|a card this small|LOW: ", re.I)
|
||||
|
||||
|
||||
def engine_stop_reason(lines) -> str | None:
|
||||
"""The engine's own last line saying a card was out of memory, without its "strata generate: " prefix; None when the start stopped for another reason.
|
||||
`lines` are the log lines of ONE start (the launcher passes only what this attempt wrote)."""
|
||||
for raw in reversed([str(x).strip() for x in lines or []]):
|
||||
if not ENGINE_LINE.match(raw) or _NOT_A_STOP.search(raw):
|
||||
continue
|
||||
if any(p.search(raw) for p in ENGINE_STOP_PATTERNS):
|
||||
return ENGINE_LINE.sub("", raw, count=1).strip()
|
||||
return None
|
||||
@@ -0,0 +1,85 @@
|
||||
// tests/program/split_walk_test.cpp - the layer split's spill walk (include/strata/program/split_walk.hpp): who is short, who is not, and what is given up in which order.
|
||||
#include "strata/program/split_walk.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
using namespace strata::program::split_walk;
|
||||
|
||||
namespace {
|
||||
int g_fail = 0;
|
||||
void check(bool ok, const std::string& what) {
|
||||
if (ok) return;
|
||||
std::fprintf(stderr, "FAIL: %s\n", what.c_str());
|
||||
++g_fail;
|
||||
}
|
||||
// IQ3_S on Qwen3.8-Flash-Next, 128K, int8 KV, window 32768, MTP: the figures the 8 GB start log gave (3485 MiB dense set, 835 MiB draft layer, 410 MiB head)
|
||||
Model iq3s() {
|
||||
Model m;
|
||||
m.dense_mib = 1466 + 2019;
|
||||
m.session_mib = 400;
|
||||
m.head_mib = 410;
|
||||
m.drafter_mib = 1000;
|
||||
m.slot_mib = 3;
|
||||
m.reserve_mib = 796;
|
||||
return m;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const Model m = iq3s();
|
||||
// 1. the trim: 8 GB cards are short, 12 GB and bigger are not (a big card never reaches the walk)
|
||||
check(trim_needed(7635, m, true), "an 8 GB card holding the head and the drafter is short");
|
||||
check(trim_needed(7635, m, false), "an 8 GB card without them is short too");
|
||||
check(!trim_needed(12288, m, true), "a 12 GB card is not short");
|
||||
check(!trim_needed(16384, m, true), "a 16 GB card is not short");
|
||||
check(!trim_needed(97000, m, true), "a 96 GB card is never short");
|
||||
check(!trim_needed(0, m, true), "an unreadable card (0 free) decides nothing");
|
||||
check(untrimmed_need_mib(m, true) > untrimmed_need_mib(m, false), "the last stage needs more");
|
||||
// monotone in the card: the threshold is one number
|
||||
int64_t edge = 0;
|
||||
for (int64_t f = 1000; f < 40000; ++f)
|
||||
if (!trim_needed(f, m, true)) { edge = f; break; }
|
||||
check(edge == untrimmed_need_mib(m, true), "the threshold is the need");
|
||||
for (int64_t f = edge; f < edge + 5000; f += 97) check(!trim_needed(f, m, true), "never short above the threshold");
|
||||
// a model with a smaller dense set (a smaller quant) is short later
|
||||
Model small = m;
|
||||
small.dense_mib = 1500;
|
||||
check(!trim_needed(7635, small, false), "a small dense set fits an 8 GB card untrimmed");
|
||||
// 2. fewer cards
|
||||
check(!card_usable(2048), "a card with 2 GB free leaves the split");
|
||||
check(card_usable(7635), "an 8 GB card stays");
|
||||
check(card_usable(kMinCardMib) && !card_usable(kMinCardMib - 1), "the edge");
|
||||
// 3. the draft layer: the single card's arithmetic, unchanged
|
||||
const uint64_t layer = 835ull << 20, head = 410ull << 20;
|
||||
check(draft_layer_need_bytes(layer, head) == layer + head + (220ull << 20) + (1550ull << 20), "the single card's need");
|
||||
check(draft_layer_short(1830ull << 20, layer, head), "1.8 GiB free: the layer goes");
|
||||
check(!draft_layer_short(3640ull << 20, layer, head), "3.6 GiB free (trimmed 8 GB card): the layer stays");
|
||||
check(!draft_layer_short(0, layer, head), "0 free is an unreadable figure");
|
||||
check(dflash2_need_bytes(715ull << 20, 213ull << 20) < draft_layer_need_bytes(layer, head), "DFlash2 needs less than the draft layer");
|
||||
// 4. the KV window walks 32768 -> 20480 -> 8192 -> 4096 -> stop, and never goes past the context or the engine's minimum
|
||||
check(next_kv_window(32768, 131072, 4096) == 20480, "32768 -> 20480");
|
||||
check(next_kv_window(20480, 131072, 4096) == 8192, "20480 -> 8192");
|
||||
check(next_kv_window(8192, 131072, 4096) == 4096, "8192 -> 4096");
|
||||
check(next_kv_window(4096, 131072, 4096) == 0, "4096 is the last");
|
||||
check(next_kv_window(0, 131072, 4096) == 20480, "the whole context in VRAM goes to 20480");
|
||||
check(next_kv_window(0, 16384, 4096) == 8192, "a context of 16K has no 20480 step");
|
||||
check(next_kv_window(0, 4096, 4096) == 0, "a context at the minimum has none");
|
||||
check(next_kv_window(8192, 131072, 8192) == 0, "a larger engine minimum stops the walk");
|
||||
// 5. the shares of the layers
|
||||
int64_t fm[2] = {7635, 7635}, out[2] = {0, 0};
|
||||
proportional_layers(fm, 2, 48, 1550, out);
|
||||
check(out[0] == 24 && out[1] == 24, "two equal cards share 48 layers 24/24");
|
||||
int64_t fm2[2] = {11000, 7000};
|
||||
proportional_layers(fm2, 2, 48, 1550, out);
|
||||
check(out[0] + out[1] == 48 && out[0] > out[1], "the bigger card takes more layers");
|
||||
int64_t fm3[3] = {8000, 8000, 8000}, out3[3];
|
||||
proportional_layers(fm3, 3, 48, 1550, out3);
|
||||
check(out3[0] == 16 && out3[1] == 16 && out3[2] == 16, "three equal cards 16/16/16");
|
||||
int64_t fm4[2] = {100000, 1700}, out4[2];
|
||||
proportional_layers(fm4, 2, 48, 1550, out4);
|
||||
check(out4[0] + out4[1] == 48 && out4[1] >= 0, "a tiny card leaves the sum right");
|
||||
if (g_fail == 0) std::printf("split_walk_test: ok\n");
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -613,6 +613,27 @@ class TheInstalledEngineDecidesTheFlags(unittest.TestCase):
|
||||
mtp_retry_args({**base, "drafting": "auto"}, old)
|
||||
self.assertEqual(drafting.take_note(), "")
|
||||
|
||||
def test_the_no_drafter_retry_exists_for_an_auto_start_with_a_drafter_and_never_swaps_in_the_mtp(self):
|
||||
"""A start that ended for lack of graphics memory is repeated with no drafter (multi-card walk): DFlash2 -> MTP would take MORE memory from a card that is short."""
|
||||
from serve.server import nodraft_retry_args
|
||||
old = str(self.exe("old.exe", self.OLD))
|
||||
base = {"model_name": GYRO, "args": list(self.args)}
|
||||
with mock.patch("serve.server.drafting_vram_mib", return_value=None), mock.patch.object(drafting, "data_dir_of", return_value=self.data):
|
||||
alt = nodraft_retry_args({**base, "drafting": "auto"}, old, ["--effort", "x"])
|
||||
self.assertIsNotNone(alt)
|
||||
for flag in ("--draft", "--draft-model", "--draft-fallback", "--mtp", "--mtp-draft-vocab"):
|
||||
self.assertNotIn(flag, alt, flag)
|
||||
self.assertEqual(alt[-2:], ["--effort", "x"], "what the start adds goes along")
|
||||
self.assertIn("--native", alt, "everything else is the same start")
|
||||
self.assertIsNotNone(nodraft_retry_args(dict(base), old), "no choice saved means Auto")
|
||||
self.assertIsNone(nodraft_retry_args({**base, "drafting": "dflash2"}, old), "named by the person: it fails with its reason")
|
||||
self.assertIsNone(nodraft_retry_args({**base, "drafting": "mtp"}, old), "named by the person: it fails with its reason")
|
||||
self.assertIsNone(nodraft_retry_args({**base, "drafting": "off"}, old), "no drafter to drop")
|
||||
drafting.take_note()
|
||||
with mock.patch("serve.server.drafting_vram_mib", return_value=None), mock.patch.object(drafting, "data_dir_of", return_value=self.data):
|
||||
nodraft_retry_args({**base, "drafting": "auto"}, old)
|
||||
self.assertEqual(drafting.take_note(), "", "working the retry out leaves the note of the real start alone")
|
||||
|
||||
def test_the_engine_sources_carry_the_words_the_program_is_read_for(self):
|
||||
src = (ROOT / "src" / "program" / "generate.cpp").read_text(encoding="utf-8")
|
||||
self.assertIn(drafting._MARK_FILE.decode(), src, "rc1's start message is what marks an engine that takes the vocabulary file alone")
|
||||
@@ -657,9 +678,9 @@ class WordsMatchTheApp(unittest.TestCase):
|
||||
catalogs = {l: {**json.loads((folder / f"{l}.json").read_text(encoding="utf-8")), **json.loads((folder / f"df2-{l}.json").read_text(encoding="utf-8"))}
|
||||
for l in ("en", "de", "es", "fr", "ja", "ko", "pt", "ru", "zh")}
|
||||
ids = {v: k for k, v in catalogs["en"].items()}
|
||||
self.assertEqual(len(drafting.REASONS), 8)
|
||||
self.assertEqual(len(drafting.REASONS), 9)
|
||||
lines = [drafting.mtp_line(r) for r in (drafting.DIDNT_FIT, drafting.NOT_HERE, drafting.NOT_SUPPORTED, drafting.NOT_DOWNLOADED, drafting.NOT_THIS_CARD, drafting.DIDNT_START)]
|
||||
lines += [drafting.line("dflash2", drafting.NO_MTP), drafting.line("off", drafting.NONE_HERE)]
|
||||
lines += [drafting.line("dflash2", drafting.NO_MTP), drafting.line("off", drafting.NONE_HERE), drafting.line("off", drafting.SHORT_OF_VRAM)]
|
||||
for line in lines:
|
||||
self.assertIn(line, ids, line)
|
||||
for lang, catalog in catalogs.items():
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Two small cards on a layer split (MULTISPILL, round 0212): what the launcher says when a card runs out of memory.
|
||||
|
||||
The final line of a failed start is the engine's own reason, not the picture reader's harmless "ggml_cuda_init: ... no CUDA-capable device" that it prints when the
|
||||
cards are full and it runs on the processor; the next step does not tell the person to change the context or the number of requests. Nothing here starts an engine."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from strata_cli.launch import runner as R
|
||||
from strata_control import vram_fit
|
||||
|
||||
# the log of the 2 x 8 GB start that failed (HYBRID, srv118, 2 x RTX PRO 6000 held to 8 GB free by hogs, 128K): the engine's reason, then the picture reader's line
|
||||
SERVE_STOP = [
|
||||
"strata serve: layer split, CUDA1 prompt path: prefill: device buffers for a chunk of 512 tokens do not fit (5 of 97247 MiB free at the failure; 555 MiB taken, the failing buffer wanted 2 MiB)",
|
||||
"strata serve: the GPU has too little free VRAM for the prompt path: turn images off (setup: --vision no), close other programs using the GPU, use a shorter context, or read prompts in smaller chunks (--prefill 512)",
|
||||
"ggml_cuda_init: failed to initialize CUDA: no CUDA-capable device is detected",
|
||||
]
|
||||
CACHE_STOP = [
|
||||
"strata generate: layer split, CUDA1: 0.17 GiB free of 94.97, room for experts 0.00 GiB",
|
||||
"strata generate: layer split, CUDA1 expert cache: no room",
|
||||
"ggml_cuda_init: failed to initialize CUDA: no CUDA-capable device is detected",
|
||||
]
|
||||
|
||||
|
||||
class TheEnginesReason(unittest.TestCase):
|
||||
def test_the_reason_is_the_engines_not_the_picture_readers(self):
|
||||
self.assertEqual(vram_fit.engine_stop_reason(SERVE_STOP),
|
||||
"the GPU has too little free VRAM for the prompt path: turn images off (setup: --vision no), close other programs using the GPU, "
|
||||
"use a shorter context, or read prompts in smaller chunks (--prefill 512)")
|
||||
self.assertEqual(vram_fit.engine_stop_reason(CACHE_STOP), "layer split, CUDA1 expert cache: no room")
|
||||
|
||||
def test_other_stops_and_the_walks_own_lines_are_not_a_memory_stop(self):
|
||||
self.assertIsNone(vram_fit.engine_stop_reason(["strata generate: something else is wrong", "ggml_cuda_init: failed to initialize CUDA"]))
|
||||
self.assertIsNone(vram_fit.engine_stop_reason([]))
|
||||
self.assertIsNone(vram_fit.engine_stop_reason(None))
|
||||
walk = ["strata generate: short of GPU memory: CUDA1 has 2.53 GiB free and a layer split card needs about 9.1 GiB: each later card loads only the dense weights of its own layers",
|
||||
"strata serve: CUDA1: 293 MiB of VRAM free with everything loaded - LOW: its window graphs are captured on first use"]
|
||||
self.assertIsNone(vram_fit.engine_stop_reason(walk), "what the walk did and a LOW warning say nothing about why a start stopped")
|
||||
|
||||
def test_the_last_memory_line_wins(self):
|
||||
lines = ["strata generate: layer split, CUDA1 expert cache: no room", "strata serve: the GPU has too little free VRAM for the prompt path: x"]
|
||||
self.assertTrue(vram_fit.engine_stop_reason(lines).startswith("the GPU has too little free VRAM"))
|
||||
|
||||
|
||||
class TheLastLine(unittest.TestCase):
|
||||
def explain(self, lines, **result):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
log = Path(tmp) / "engine.log"
|
||||
log.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
cfg = Path(tmp) / "run.json"
|
||||
cfg.write_text(json.dumps({"exe": "e", "args": [], "log": str(log)}), encoding="utf-8")
|
||||
return R.explain_engine_exit({"state": "failed", "error": "server_exited", "engine_exit_code": 1, **result}, cfg)
|
||||
|
||||
def test_a_prompt_path_stop_shows_the_engines_line_and_a_next_step_that_keeps_the_context(self):
|
||||
text, nxt = self.explain(SERVE_STOP)
|
||||
self.assertIn("too little free VRAM for the prompt path", text)
|
||||
self.assertNotIn("ggml_cuda_init", text)
|
||||
self.assertNotIn("no CUDA-capable device", text)
|
||||
self.assertIn("were not changed", nxt, "the context and the requests stay what the person chose")
|
||||
self.assertIn("fewer cards", nxt)
|
||||
self.assertNotIn("--context 16k", nxt, "the old advice asked for a smaller context")
|
||||
|
||||
def test_a_stage_cache_stop_shows_the_engines_line(self):
|
||||
text, _ = self.explain(CACHE_STOP)
|
||||
self.assertIn("layer split, CUDA1 expert cache: no room", text)
|
||||
self.assertNotIn("ggml_cuda_init", text)
|
||||
|
||||
def test_a_stop_for_another_reason_is_worded_as_before(self):
|
||||
text, nxt = self.explain(["strata generate: cudaMalloc failed", "unsupported quant type 99"])
|
||||
self.assertIn("unsupported quant type 99", text)
|
||||
self.assertNotIn("were not changed", nxt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user