sgl-project/sglang · error · std::runtime_error
finishExternalCorpusLoad called without startExternalCorpusL
Error message
finishExternalCorpusLoad called without startExternalCorpusLoad
What it means
finishExternalCorpusLoad finalizes the staged suffix automaton and installs it under a corpus id. It throws if no staging automaton exists, meaning startExternalCorpusLoad was never called or the staging slot was already consumed by a prior finish.
Source
Thrown at python/sglang/kernels/jit/csrc/ngram_corpus/ngram.cpp:88
// staging_sam_ is disjoint from sams_ / trie_. Only finishExternalCorpusLoad
// briefly acquires mutex_ when moving the completed SAM into sams_.
void Ngram::startExternalCorpusLoad() {
if (staging_sam_) {
throw std::runtime_error("startExternalCorpusLoad called while another load is in progress");
}
staging_sam_ = std::make_unique<SuffixAutomaton>();
}
void Ngram::appendExternalCorpusTokens(const std::vector<int32_t>& tokens) {
if (!staging_sam_) {
throw std::runtime_error("appendExternalCorpusTokens called without startExternalCorpusLoad");
}
staging_sam_->appendTokens(tokens);
}
void Ngram::finishExternalCorpusLoad(const std::string& corpus_id) {
if (!staging_sam_) {
throw std::runtime_error("finishExternalCorpusLoad called without startExternalCorpusLoad");
}
staging_sam_->finalize();
if (staging_sam_->empty()) {
staging_sam_.reset();
throw std::runtime_error("External corpus is empty — no tokens were loaded.");
}
// Only lock briefly to install the completed SAM.
std::unique_lock<std::mutex> lock(mutex_);
if (sams_.find(corpus_id) != sams_.end()) {
throw std::runtime_error(
"External corpus '" + corpus_id + "' already exists. Remove it before adding a new corpus with the same id.");
}
sams_.emplace(corpus_id, std::move(staging_sam_));
}
void Ngram::removeExternalCorpus(const std::string& corpus_id) {
std::unique_lock<std::mutex> lock(mutex_);
sams_.erase(corpus_id);View on GitHub (pinned to 0132848349)
Solutions
- Call finish exactly once per successful start
- Track load state (started/finished) in the caller and skip redundant finish calls
- On error paths, only reset via the library's own semantics — do not re-finish
Example fix
// before
ngram.finishExternalCorpusLoad("wiki");
ngram.finishExternalCorpusLoad("wiki"); // throws
// after
ngram.finishExternalCorpusLoad("wiki");
finished = true; // guard later calls Defensive patterns
Strategy: validation
Validate before calling
if (load_started && !load_finished) ngram.finishExternalCorpusLoad(id);
Try / catch
try { ngram.finishExternalCorpusLoad(id); } catch (const std::runtime_error& e) { /* treat missing-start as no-op, log */ } Prevention
- Track started/finished booleans per load
- Never call finish from both success and cleanup paths
When it happens
Trigger: Calling finishExternalCorpusLoad(id) without a successful startExternalCorpusLoad(), or calling finish twice.
Common situations: Double-finish in an async loader (e.g. cleanup path plus normal path); exception handler that finishes an already-finished load; start failed or was skipped due to earlier error.
Related errors
- appendExternalCorpusTokens called without startExternalCorpu
- Missing previous frame for delta payload
- startExternalCorpusLoad called while another load is in prog
- External ngram corpus exceeds the remaining token budget ({m
- External corpus is empty — no tokens were loaded.
AI-assisted analysis of sgl-project/sglang@0132848349 (2026-08-28).
Data as JSON: /api/errors/238f1d7950672307.
Report an issue: GitHub.