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

  1. Call finish exactly once per successful start
  2. Track load state (started/finished) in the caller and skip redundant finish calls
  3. 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

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


AI-assisted analysis of sgl-project/sglang@0132848349 (2026-08-28). Data as JSON: /api/errors/238f1d7950672307. Report an issue: GitHub.