Repository navigation
Description
Environment
| OS | Windows 11 Pro build 26200 |
| GPU | AMD Radeon RX 7900 XT (gfx1100, 20 GB), driver 32.0.31041.1004 |
| Backend | HIP (--backend hip), prebuilt 0.1.41 |
| Engine | 0.1.41 |
| Model | Qwen3.8-Flash-Next IQ3_XXS |
| CPU / RAM | i5-12600KF (AVX2), 96 GB DDR4 |
Relevant args: --max-context 262144 --kv int8 --kv-resident 32768 --spec 4 --prefill auto:32768 --expert-cache 4096 --vram-reserve-mib 700, plus a CPU vision encoder.
What happened
A single request with a 240,903-token prompt died part way through prefill with a 400 the client cannot explain, and the engine had to reload the model:
[strata] reading the prompt: 194,560 of 240,903 tokens, 192 s so far
[strata] the engine reported an error: saving a conversation checkpoint failed
[strata] done: 0 tokens in 192 s (0.0 tok/s) (error, cancel=False)
[strata] 400 invalid request: saving a conversation checkpoint failed
[strata] model unloaded; the next request loads it again
The cause is in the engine log, one line above the summary:
strata serve: prompt chunk auto: 19456 tokens, a 384-slot ring
strata serve: the prompt path borrows 5177 CUDA0 cache slots (8.39 GiB)
strata serve: 2673 MiB of VRAM free with everything loaded
strata serve: checkpoint save: conversation snapshot running-state copy: unspecified launch failure
strata serve: saving a conversation checkpoint failed
hipModuleUnload failed:
error: unspecified launch failure
hipModuleUnload failed:
error: unspecified launch failure
hipModuleUnload failed:
error: unspecified launch failure
Note where it failed: 194560 = 10 x 19456 — exactly the end of the 10th prompt chunk, i.e. the periodic checkpoint save. 2673 MiB of VRAM was free, so this is not VRAM exhaustion.
I have seen this once (0.1.41, 2026-10-11). I am not able to reproduce it on demand; many other 240K prefills on the same machine and config went through fine, before and after.
Two things that look like real defects, independent of the fault itself
1. gpu_sync_ok() passes on an already-faulted device, so the message misattributes the failure.
src/program/generate.cpp:7217-7223:
// #613: why the last checkpoint_at failed - a failed device sync is the GPU itself (a hang Windows then resets),
// not the checkpoint, and the error says so
std::string ckpt_why;
auto gpu_sync_ok = [&]() -> bool {
const cudaError_t e = cudaDeviceSynchronize();
if (e == cudaSuccess) return true;
ckpt_why = std::string(": the GPU stopped responding before it (") + cudaGetErrorString(e) +
") - a GPU hang; on Windows the driver is then reset";
return false;
};Here cudaDeviceSynchronize() succeeded — ckpt_why stayed empty, which is why the request's error carries no reason — and the very next operation failed:
if (!gpu_sync_ok() || !checkpoint_save(c, ss, g)) return false; // generate.cpp:7247checkpoint_save reported conversation snapshot running-state copy: unspecified launch failure, i.e. a device fault (hipErrorLaunchFailure) on the copy. The three hipModuleUnload failed lines immediately after confirm the context really was dead. So the #613 discrimination did not fire on a case that was, in fact, the GPU — it just reported a bare "checkpoint failed", which points the reader at the checkpoint code instead.
2. The reason never reaches the client, only stderr.
src/program/generate.cpp:1380-1385:
bool checkpoint_save(ConvCheckpoint& c, const SessionState& ss, const ModelGeometry& g) {
std::string error;
if (strata::core::conversation_checkpoint_save(c, ss, g, error)) return true;
std::fprintf(stderr, "strata serve: checkpoint save: %s\n", error.c_str()); // the caller's ERR has no reason
return false;
}The comment acknowledges the gap. From an API client's point of view a failed checkpoint save is indistinguishable from any other 400, and since the request took 192 s to get there, there is nothing to go on. I only found the reason because this machine keeps the engine's stderr in a log file. Would it be acceptable to append ckpt_why (or the error string) to the caller's e unconditionally, the way the GPU-hang path already does?
Question rather than a claim
Is aborting the whole request the intended response to a failed checkpoint save? A checkpoint is only a cache for the next continuation, so in principle the current prefill could finish and merely lose reuse. Here it may be moot — the context was already faulted and the engine reloaded anyway — but if checkpoint_at() can also fail for benign reasons, discarding a 240K prefill (192 s) over a failed cache save is a harsh trade.
I have no evidence the checkpoint save caused the fault; it may simply be the first device operation after it. If it is any help, the common factor with #613 is a 240K-class prefill on Windows + gfx1100 with the cache lent to the prompt path (the prompt path borrows 5177 CUDA0 cache slots), and the failure landed exactly on a chunk boundary where the save happens.
Activity
talisp commentedon Oct 14, 2026
Another data point for the same failure family, on a different setup: 2x NVIDIA A2 16 GB (CUDA, Linux, v0.1.42), Q8_0 (188 GB, experts streamed from SSD, 62 GiB RAM),
--vram-reserve-mib 700,--pipeline-windows 2.saving a checkpoint part failedand the engine exited (code 1). The reason was only in the engine's stderr:checkpoint save: conversation snapshot running-state copy: out of memory. So the same "no reason reaches the client" as here, with an out-of-memory instead of a device fault.mtp prefill: the input records do not fit(cudaMalloc,src/core/mtp.cpp~1138/1180 in 0.1.42). In another run of the same case the message wasmtp: step capture: no error, which gives no cause at all.Suggestion, in the same direction as this issue: put the engine's reason string, the size it asked for and the device's free memory in the line the client sees.
With
--vram-reserve-mib 1000we have not seen either failure again (Q8_0, 64K to 1M, one run per context). Not re-tested on 0.1.43 or later.Numbers and context for the whole battery: #2446 (drafted by an AI assistant from our logs; the account owner reads replies).
Niko1221 commentedon Oct 19, 2026
Thanks for taking the time to report this.
We're resetting the tracker for 0.2.0-beta. There are more than 1,300 open issues and pull requests, too many for us to read properly, so we're closing everything that isn't being worked on right now. Closed here does not mean rejected.
To reopen it takes about two minutes:
/reopen. A check runs and either reopens the issue or lists what is still missing.The rules are in CONTRIBUTING.md.