diff --git a/server/src/common/daemon_loop.cpp b/server/src/common/daemon_loop.cpp index 1abcf0c52..a0defdc18 100644 --- a/server/src/common/daemon_loop.cpp +++ b/server/src/common/daemon_loop.cpp @@ -139,6 +139,17 @@ static bool looks_like_path(const std::string & s) { return s.find('/') != std::string::npos; } +static void emit_generate_error(const GenerateResult & result) { + std::fprintf(stderr, "[daemon] generation failed: code=%s", + result.error_code().data()); + if (!result.error_detail().empty()) { + std::fprintf(stderr, " detail=%s", result.error_detail().data()); + } + std::fprintf(stderr, "\n"); + std::printf("err %s\n", result.error_code().data()); + std::fflush(stdout); +} + // Read a prompt file: raw int32 stream (file size implies token count). static std::vector read_uncounted_i32(const std::string & path) { std::ifstream f(path, std::ios::binary | std::ios::ate); @@ -318,9 +329,8 @@ int run_daemon(ModelBackend & backend, const DaemonLoopArgs & args) { req.stream = false; auto result = backend.generate(req, io); - if (!result.ok) { - std::printf("err %s\n", result.error.c_str()); - std::fflush(stdout); + if (!result.ok()) { + emit_generate_error(result); continue; } if (!write_counted_i32(out_path, result.tokens)) { @@ -375,9 +385,8 @@ int run_daemon(ModelBackend & backend, const DaemonLoopArgs & args) { req.snap_slot = snap_slot; auto result = backend.restore_and_generate(slot, req, io); - if (!result.ok) { - std::printf("err %s\n", result.error.c_str()); - std::fflush(stdout); + if (!result.ok()) { + emit_generate_error(result); io.emit(-1); continue; } @@ -424,10 +433,9 @@ int run_daemon(ModelBackend & backend, const DaemonLoopArgs & args) { req.snap_slot = snap_slot; auto result = backend.generate(req, io); - if (!result.ok) { + if (!result.ok()) { io.emit(-1); - std::printf("err %s\n", result.error.c_str()); - std::fflush(stdout); + emit_generate_error(result); continue; } std::printf("ok N=%d gen=%zu prefill_s=%.3f decode_s=%.3f decode_tok_s=%.1f stream_fd=%d\n", diff --git a/server/src/common/layer_split_backend.cpp b/server/src/common/layer_split_backend.cpp index c9eb71edd..94e7e13a7 100644 --- a/server/src/common/layer_split_backend.cpp +++ b/server/src/common/layer_split_backend.cpp @@ -48,18 +48,18 @@ GenerateResult LayerSplitBackend::run_from_state(const GenerateRequest & req, bool reset_state) { GenerateResult result; if (!adapter_) { - result.error = "adapter_unavailable"; + result.fail(GenerateErrorCode::AdapterUnavailable); return result; } DaemonIO out_io = io.with_token_callback(req.on_token); if (base_pos + (int)req.prompt.size() + req.n_gen + 1 > adapter_->max_context()) { - result.error = "context_overflow"; + result.fail(GenerateErrorCode::ContextOverflow); return result; } if (req.do_sample && req.sampler.needs_logit_processing() && !adapter_->supports_cpu_sampling()) { - result.error = "sampling_unsupported"; + result.fail(GenerateErrorCode::SamplingUnsupported); return result; } @@ -86,7 +86,7 @@ GenerateResult LayerSplitBackend::run_from_state(const GenerateRequest & req, std::vector chunk(req.prompt.begin() + consumed, req.prompt.begin() + consumed + n_tokens); if (!adapter_->prefill(chunk, base_pos + consumed, last_tok)) { - result.error = "prefill_failed"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } consumed += n_tokens; @@ -104,7 +104,7 @@ GenerateResult LayerSplitBackend::run_from_state(const GenerateRequest & req, if (req.n_gen > 0) { if (last_tok < 0) { - result.error = "decode_seed_missing"; + result.fail(GenerateErrorCode::DecodeSeedMissing); return result; } auto t_decode_start = std::chrono::steady_clock::now(); @@ -118,14 +118,14 @@ GenerateResult LayerSplitBackend::run_from_state(const GenerateRequest & req, result.tokens, out_io); if (use_dflash) result.accept_rate = dflash_accept_rate; if (!ok) { - result.error = "decode_failed"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } result.decode_s = std::chrono::duration( std::chrono::steady_clock::now() - t_decode_start).count(); } - result.ok = true; + result.succeed(); return result; } @@ -166,7 +166,7 @@ GenerateResult LayerSplitBackend::restore_and_generate_impl( int slot, const GenerateRequest & req, const DaemonIO & io) { GenerateResult result; if (!adapter_ || !adapter_->snapshot_restore(slot)) { - result.error = "invalid_snapshot_slot"; + result.fail(GenerateErrorCode::InvalidSnapshotSlot); io.emit(-1); return result; } diff --git a/server/src/common/model_backend.h b/server/src/common/model_backend.h index b88f6f21f..b7058e497 100644 --- a/server/src/common/model_backend.h +++ b/server/src/common/model_backend.h @@ -13,7 +13,10 @@ #include #include #include +#include #include +#include +#include #include #include "ggml.h" @@ -126,9 +129,48 @@ struct GenerateRequest { bool force_ar_decode = false; }; +// Stable, backend-independent generation failure categories. Backends should +// use these for recurrent failures so callers do not need to understand +// architecture-specific strings. `generate_error_code()` is the daemon/API +// wire representation and must remain backward-compatible once published. +enum class GenerateErrorCode { + Incomplete, + AdapterUnavailable, + ContextOverflow, + SamplingUnsupported, + PrefillFailed, + DecodeSeedMissing, + DecodeFailed, + InvalidSnapshotSlot, + ModelParked, + BackendSpecific, +}; + +constexpr std::string_view generate_error_code(GenerateErrorCode error) { + switch (error) { + case GenerateErrorCode::Incomplete: return "incomplete"; + case GenerateErrorCode::AdapterUnavailable: return "adapter_unavailable"; + case GenerateErrorCode::ContextOverflow: return "context_overflow"; + case GenerateErrorCode::SamplingUnsupported: return "sampling_unsupported"; + case GenerateErrorCode::PrefillFailed: return "prefill_failed"; + case GenerateErrorCode::DecodeSeedMissing: return "decode_seed_missing"; + case GenerateErrorCode::DecodeFailed: return "decode_failed"; + case GenerateErrorCode::InvalidSnapshotSlot: return "invalid_snapshot_slot"; + case GenerateErrorCode::ModelParked: return "model_parked"; + case GenerateErrorCode::BackendSpecific: return "backend_specific"; + } + return "unknown_error"; +} + +struct GenerateError { + GenerateErrorCode code = GenerateErrorCode::Incomplete; + std::string detail; +}; + struct GenerateResult { - bool ok = false; - std::string error; // "prefill", "decode", etc. + // Default to an incomplete failure so a backend must explicitly call + // succeed() before returning a successful result. + std::optional error = GenerateError{}; std::vector tokens; double prefill_s = 0.0; double decode_s = 0.0; @@ -155,6 +197,26 @@ struct GenerateResult { // to zero output for clients and should take the same AR retry path as // an empty token vector. bool empty_visible_output = false; + + bool ok() const { + return !error.has_value(); + } + + std::string_view error_code() const { + return error ? generate_error_code(error->code) : std::string_view{}; + } + + std::string_view error_detail() const { + return error ? std::string_view(error->detail) : std::string_view{}; + } + + void succeed() { + error.reset(); + } + + void fail(GenerateErrorCode code, std::string detail = {}) { + error = GenerateError{code, std::move(detail)}; + } }; // ─── Backend interface ────────────────────────────────────────────────── @@ -226,7 +288,7 @@ struct ModelBackend { const GenerateResult & result) { return req.n_gen > 0 && !req.force_ar_decode - && result.ok + && result.ok() && result.spec_decode_ran && (result.tokens.empty() || result.empty_visible_output); } diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index 9b4616da1..e75a49ea9 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -670,13 +670,13 @@ GenerateResult DeepSeek4Backend::generate_impl(const GenerateRequest & req, // Prefill int committed = do_prefill(req.prompt, io); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } result.prefill_s = elapsed_s(t0); if (req.n_gen <= 0) { - result.ok = true; + result.succeed(); maybe_save_routing_stats(); return result; } @@ -689,11 +689,11 @@ GenerateResult DeepSeek4Backend::generate_impl(const GenerateRequest & req, bool forced_close = false; if (!do_decode(committed, req.n_gen, gen_tokens, io, req.budget_hook, &forced_close)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } - result.ok = true; + result.succeed(); result.tokens = std::move(gen_tokens); result.decode_s = elapsed_s(t1); result.budget_forced_close = forced_close; diff --git a/server/src/gemma4/gemma4_backend.cpp b/server/src/gemma4/gemma4_backend.cpp index 9e7f131a4..3f8a457d1 100644 --- a/server/src/gemma4/gemma4_backend.cpp +++ b/server/src/gemma4/gemma4_backend.cpp @@ -724,7 +724,7 @@ bool Gemma4Backend::do_spec_decode(int committed, int n_gen, GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, const DaemonIO & io) { GenerateResult result; - if (parked_) { result.error = "model is parked"; return result; } + if (parked_) { result.fail(GenerateErrorCode::ModelParked); return result; } DaemonIO out_io = io.with_token_callback(req.on_token); sampler_ = req.sampler; @@ -742,7 +742,7 @@ GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, result.prefill_s = std::chrono::duration( std::chrono::steady_clock::now() - t_prefill_start).count(); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } @@ -771,7 +771,7 @@ GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, &req.budget_hook, &result.budget_forced_close, &result.accept_rate)) { - result.error = "spec_decode"; + result.fail(GenerateErrorCode::BackendSpecific, "spec_decode"); return result; } } else { @@ -789,7 +789,7 @@ GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, if (!gemma4_step(backend_, w_, cache_, embed_buf.data(), &last_tok, 1, committed - 1, logits, kvflash_active() ? &kvflash_pager_ : nullptr)) { - result.error = "first logits"; + result.fail(GenerateErrorCode::BackendSpecific, "first logits"); return result; } @@ -809,13 +809,13 @@ GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, out_io.emit(first); if (out_io.cancelled) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } if (first == w_.eos_id || first == w_.eos_chat_id) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } @@ -823,7 +823,7 @@ GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, if (!do_decode(committed, req.n_gen - 1, result.tokens, out_io, req.budget_hook, &result.budget_forced_close)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } } @@ -834,7 +834,7 @@ GenerateResult Gemma4Backend::generate_impl(const GenerateRequest & req, } result.decode_s = std::chrono::duration( std::chrono::steady_clock::now() - t_decode_start).count(); - result.ok = true; + result.succeed(); return result; } @@ -844,12 +844,12 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, const GenerateRequest & req, const DaemonIO & io) { GenerateResult result; - if (parked_) { result.error = "model is parked"; return result; } + if (parked_) { result.fail(GenerateErrorCode::ModelParked); return result; } DaemonIO out_io = io.with_token_callback(req.on_token); if (slot < 0 || slot >= PREFIX_SLOTS || !snapshots_[slot].ctx) { - result.error = "bad slot"; + result.fail(GenerateErrorCode::InvalidSnapshotSlot); out_io.emit(-1); return result; } @@ -895,12 +895,12 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, if (snap_pos > kvflash_tokens_ - kvflash_pager_.chunk_tokens()) { std::fprintf(stderr, "[kvflash] restored prefix (%d) exceeds pool %d\n", snap_pos, kvflash_tokens_); - result.error = "kvflash: prompt exceeds resident pool"; + result.fail(GenerateErrorCode::ContextOverflow); return result; } kvflash_pager_.reset(); if (!kvflash_alloc_span(0, snap_pos)) { - result.error = "kvflash_slot"; + result.fail(GenerateErrorCode::BackendSpecific, "kvflash_slot"); return result; } } @@ -924,7 +924,7 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, std::vector delta(req.prompt.begin() + snap_pos, req.prompt.end()); committed = do_prefill(delta, out_io, /*kv_offset=*/snap_pos); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } } else if (prompt_len > 0 && prompt_len < snap_pos) { @@ -936,7 +936,7 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, cache_.cur_pos = 0; committed = do_prefill(req.prompt, out_io, /*kv_offset=*/0); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } } @@ -977,7 +977,7 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, &req.budget_hook, &result.budget_forced_close, &result.accept_rate)) { - result.error = "spec_decode"; + result.fail(GenerateErrorCode::BackendSpecific, "spec_decode"); return result; } } else { @@ -995,7 +995,7 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, if (!gemma4_step(backend_, w_, cache_, embed_buf.data(), &last_tok, 1, committed - 1, logits, kvflash_active() ? &kvflash_pager_ : nullptr)) { - result.error = "first logits"; + result.fail(GenerateErrorCode::BackendSpecific, "first logits"); return result; } @@ -1015,13 +1015,13 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, out_io.emit(first); if (out_io.cancelled) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } if (first == w_.eos_id || first == w_.eos_chat_id) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } @@ -1029,7 +1029,7 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, if (!do_decode(committed, req.n_gen - 1, result.tokens, out_io, req.budget_hook, &result.budget_forced_close)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } } @@ -1040,7 +1040,7 @@ GenerateResult Gemma4Backend::restore_and_generate_impl(int slot, } result.decode_s = std::chrono::duration( std::chrono::steady_clock::now() - t_decode_start).count(); - result.ok = true; + result.succeed(); return result; } diff --git a/server/src/laguna/laguna_backend.cpp b/server/src/laguna/laguna_backend.cpp index 22ffdab21..ae78a2692 100644 --- a/server/src/laguna/laguna_backend.cpp +++ b/server/src/laguna/laguna_backend.cpp @@ -1282,7 +1282,7 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, const DaemonIO & io) { if (hybrid_mode_ && moe_hybrid_) { auto result = generate_hybrid(req, io); - if (result.ok) { + if (result.ok()) { // Flush routing-frequency profile if requested (independent of swap). if (!routing_stats_out_path_.empty() && routing_stats_) { std::string serr; @@ -1307,7 +1307,7 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, } if (N + req.n_gen > args_.max_ctx) { - result.error = "overflow"; + result.fail(GenerateErrorCode::ContextOverflow); return result; } @@ -1332,7 +1332,7 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, // ── Prefill ── std::vector embed_pf((size_t)N * w_.n_embd); if (!w_.embedder.embed(req.prompt.data(), N, embed_pf.data())) { - result.error = "embed_prefill"; + result.fail(GenerateErrorCode::BackendSpecific, "embed_prefill"); return result; } @@ -1370,7 +1370,7 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, n_tok, kv_start, no_mask, last_logits, kvf, /*capture=*/true); } - if (!ok) { result.error = "prefill"; return result; } + if (!ok) { result.fail(GenerateErrorCode::PrefillFailed); return result; } auto t_pf1 = std::chrono::steady_clock::now(); result.prefill_s = std::chrono::duration(t_pf1 - t_pf0).count(); @@ -1435,7 +1435,7 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, auto t_g0 = std::chrono::steady_clock::now(); if (!draft_feature_mirror_sync_tail(cache_.target_feat, cache_.target_feat_cap, feature_mirror_, N)) { - result.error = "feature_sync"; + result.fail(GenerateErrorCode::BackendSpecific, "feature_sync"); return result; } result.spec_decode_ran = true; @@ -1444,12 +1444,12 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, &result.budget_forced_close, &result.accept_rate, &history)) { - result.error = "spec_decode"; + result.fail(GenerateErrorCode::BackendSpecific, "spec_decode"); return result; } auto t_g1 = std::chrono::steady_clock::now(); result.decode_s = std::chrono::duration(t_g1 - t_g0).count(); - result.ok = true; + result.succeed(); return result; } @@ -1527,9 +1527,9 @@ GenerateResult LagunaBackend::generate_impl(const GenerateRequest & req, result.decode_s = std::chrono::duration(t_g1 - t_g0).count(); if (should_emit) out_io.emit(-1); - if (!ok) { result.error = "decode"; return result; } + if (!ok) { result.fail(GenerateErrorCode::DecodeFailed); return result; } - result.ok = true; + result.succeed(); return result; } @@ -1549,7 +1549,7 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, if (!laguna_snapshot_restore(snapshots_[slot], cache_)) { std::fprintf(stderr, "[snap] RESTORE slot=%d: %s\n", slot, dflash27b_last_error()); - result.error = "restore"; + result.fail(GenerateErrorCode::BackendSpecific, "restore"); return result; } @@ -1558,7 +1558,7 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, if (N < prefix_len) { std::fprintf(stderr, "[snap] RESTORE prompt shorter than cached prefix (%d < %d)\n", N, prefix_len); - result.error = "prefix_shorter"; + result.fail(GenerateErrorCode::BackendSpecific, "prefix_shorter"); return result; } @@ -1568,13 +1568,13 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, N > kvflash_tokens_ - kvflash_pager_.chunk_tokens()) { std::fprintf(stderr, "[kvflash] restore prompt (%d) exceeds pool %d; " "raise --kvflash\n", N, kvflash_tokens_); - result.error = "kvflash: prompt exceeds resident pool"; + result.fail(GenerateErrorCode::ContextOverflow); return result; } if (kvflash_active()) { kvflash_pager_.reset(); if (!kvflash_alloc_span(0, prefix_len)) { - result.error = "kvflash_slot"; + result.fail(GenerateErrorCode::BackendSpecific, "kvflash_slot"); return result; } } @@ -1583,7 +1583,10 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, // Re-prefill diff tokens (or last cached token when diff is empty). bool restore_only = false; if (prefix_len == N) { - if (prefix_len <= 0) { result.error = "empty_diff"; return result; } + if (prefix_len <= 0) { + result.fail(GenerateErrorCode::BackendSpecific, "empty_diff"); + return result; + } cache_.cur_pos = prefix_len - 1; restore_only = true; } @@ -1604,7 +1607,7 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, std::vector embed_diff((size_t)diff_n * w_.n_embd); if (!w_.embedder.embed(diff_src, diff_n, embed_diff.data())) { - result.error = "embed_prefill"; + result.fail(GenerateErrorCode::BackendSpecific, "embed_prefill"); return result; } @@ -1621,7 +1624,7 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, n_tok, starts, no_mask, last_logits, kvf, /*capture=*/true); } - if (!ok) { result.error = "prefill"; return result; } + if (!ok) { result.fail(GenerateErrorCode::PrefillFailed); return result; } // ── Decode ── auto argmax = [](const std::vector & ll) { @@ -1659,7 +1662,7 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, auto t_g0 = std::chrono::steady_clock::now(); if (!draft_feature_mirror_sync_tail(cache_.target_feat, cache_.target_feat_cap, feature_mirror_, committed)) { - result.error = "feature_sync"; + result.fail(GenerateErrorCode::BackendSpecific, "feature_sync"); return result; } result.spec_decode_ran = true; @@ -1668,12 +1671,12 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, &result.budget_forced_close, &result.accept_rate, &history)) { - result.error = "spec_decode"; + result.fail(GenerateErrorCode::BackendSpecific, "spec_decode"); return result; } auto t_g1 = std::chrono::steady_clock::now(); result.decode_s = std::chrono::duration(t_g1 - t_g0).count(); - result.ok = true; + result.succeed(); return result; } @@ -1746,9 +1749,9 @@ GenerateResult LagunaBackend::restore_and_generate_impl(int slot, result.decode_s = std::chrono::duration(t_g1 - t_g0).count(); out_io.emit(-1); - if (!ok) { result.error = "decode"; return result; } + if (!ok) { result.fail(GenerateErrorCode::DecodeFailed); return result; } - result.ok = true; + result.succeed(); return result; } @@ -2708,7 +2711,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, const int N = (int)req.prompt.size(); if (N + req.n_gen > args_.max_ctx) { - result.error = "overflow"; + result.fail(GenerateErrorCode::ContextOverflow); return result; } @@ -2719,7 +2722,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, N > kvflash_tokens_ - kvflash_pager_.chunk_tokens()) { std::fprintf(stderr, "[kvflash] hybrid prompt (%d) exceeds pool %d; " "raise --kvflash\n", N, kvflash_tokens_); - result.error = "kvflash: prompt exceeds resident pool"; + result.fail(GenerateErrorCode::ContextOverflow); return result; } @@ -2727,12 +2730,12 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, if (kvflash_active()) { kvflash_pager_.reset(); if (!kvflash_alloc_span(0, N)) { - result.error = "kvflash_slot"; + result.fail(GenerateErrorCode::BackendSpecific, "kvflash_slot"); return result; } } if (!ensure_moe_expert_compute()) { - result.error = "moe_expert_compute"; + result.fail(GenerateErrorCode::BackendSpecific, "moe_expert_compute"); return result; } @@ -2743,7 +2746,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, std::vector embed_all((size_t)N * (size_t)hidden); if (!w_.embedder.embed(req.prompt.data(), N, embed_all.data())) { - result.error = "embed_prefill"; + result.fail(GenerateErrorCode::BackendSpecific, "embed_prefill"); return result; } @@ -2764,7 +2767,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, step_graph_free(prefill_sg); // reset ctx/graph but keep gallocr buffer if (!build_laguna_layer_prefn_step(prefill_sg, w_, cache_, backend_, il, chunk_start, chunk_len)) { - result.error = "prefill_build"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill_build"); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -2800,7 +2803,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, // Compute pre-FFN graph auto st = ggml_backend_graph_compute(backend_, prefill_sg.gf); if (st != GGML_STATUS_SUCCESS) { - result.error = "prefill_compute"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill_compute"); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -2829,7 +2832,8 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, ggml_tensor * sel_tensor = prefill_sg.moe_selected.empty() ? nullptr : prefill_sg.moe_selected[0]; if (!sel_tensor || !prefill_sg.moe_weights) { - result.error = "prefill_router_outputs"; + result.fail(GenerateErrorCode::BackendSpecific, + "prefill_router_outputs"); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -2867,6 +2871,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, const MoeExpertLayer * expert_layer = expert_runtime_.layer_ptr((size_t)il); std::vector ffn_batch_out; + std::string ffn_error; bool ffn_ok = false; if (storage.cold_expert_ids.empty()) { @@ -2876,7 +2881,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, chunk_post.data(), chunk_selected.data(), chunk_weights.data(), - chunk_len, ffn_batch_out, &result.error, + chunk_len, ffn_batch_out, &ffn_error, &ffn_hot_alloc, &ffn_cold_alloc, expert_compute, expert_layer); } else if (storage.all_routed_are_hot(chunk_selected.data(), @@ -2887,7 +2892,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, chunk_post.data(), chunk_selected.data(), chunk_weights.data(), - chunk_len, ffn_batch_out, &result.error, + chunk_len, ffn_batch_out, &ffn_error, &ffn_hot_alloc); } else if (moe_hybrid_->has_mmap() && !moe_hybrid_->layer_regions.empty() && @@ -2905,7 +2910,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, chunk_post.data(), chunk_selected.data(), chunk_weights.data(), - chunk_len, ffn_batch_out, &result.error, + chunk_len, ffn_batch_out, &ffn_error, &ffn_hot_alloc, &ffn_cold_alloc, expert_compute, expert_layer); } else { @@ -2915,12 +2920,14 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, chunk_post.data(), chunk_selected.data(), chunk_weights.data(), - chunk_len, ffn_batch_out, &result.error, + chunk_len, ffn_batch_out, &ffn_error, &ffn_hot_alloc, &ffn_cold_alloc, expert_compute, expert_layer); } if (!ffn_ok) { + result.fail(GenerateErrorCode::BackendSpecific, + std::move(ffn_error)); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -2965,7 +2972,8 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, if (!ggml_gallocr_alloc_graph(alloc, gf)) { ggml_gallocr_free(alloc); ggml_free(ctx); - result.error = "prefill_logits_alloc"; + result.fail(GenerateErrorCode::BackendSpecific, + "prefill_logits_alloc"); return result; } // Set last token's hidden state @@ -2975,7 +2983,8 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, if (ggml_backend_graph_compute(backend_, gf) != GGML_STATUS_SUCCESS) { ggml_gallocr_free(alloc); ggml_free(ctx); - result.error = "prefill_logits_compute"; + result.fail(GenerateErrorCode::BackendSpecific, + "prefill_logits_compute"); return result; } last_logits.resize((size_t)w_.embedder.n_vocab); @@ -3052,7 +3061,7 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, // Hybrid forward: one token through all layers int32_t argmax_tok = 0; if (!hybrid_forward_one_token(next_tok, cache_.cur_pos, act_cur, argmax_tok)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); break; } cache_.cur_pos++; @@ -3071,7 +3080,9 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req, result.decode_s = std::chrono::duration(t_g1 - t_g0).count(); if (should_emit) out_io.emit(-1); - result.ok = (result.error.empty()); + if (result.error && result.error->code == GenerateErrorCode::Incomplete) { + result.succeed(); + } return result; } diff --git a/server/src/qwen3/qwen3_backend.cpp b/server/src/qwen3/qwen3_backend.cpp index fa993ebfd..21f7f4b46 100644 --- a/server/src/qwen3/qwen3_backend.cpp +++ b/server/src/qwen3/qwen3_backend.cpp @@ -525,7 +525,7 @@ GenerateResult Qwen3Backend::generate_impl(const GenerateRequest & req, // Prefill const int committed = do_prefill(req.prompt, out_io); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } @@ -567,7 +567,7 @@ GenerateResult Qwen3Backend::generate_impl(const GenerateRequest & req, ggml_backend_get_default_buffer_type(backend_)); if (!ggml_gallocr_alloc_graph(galloc, gf)) { ggml_gallocr_free(galloc); ggml_free(ectx); - result.error = "embed alloc"; return result; + result.fail(GenerateErrorCode::BackendSpecific, "embed alloc"); return result; } ggml_backend_tensor_set(ids, &last_tok, 0, sizeof(int32_t)); ggml_backend_graph_compute(backend_, gf); @@ -578,7 +578,7 @@ GenerateResult Qwen3Backend::generate_impl(const GenerateRequest & req, // Re-step at committed-1 to get logits (KV already written, idempotent) if (!do_step(embed_buf.data(), 1, committed - 1, logits)) { - result.error = "first logits"; + result.fail(GenerateErrorCode::BackendSpecific, "first logits"); return result; } @@ -598,13 +598,13 @@ GenerateResult Qwen3Backend::generate_impl(const GenerateRequest & req, out_io.emit(first); if (out_io.cancelled) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } if (first == 151643 || first == 151645) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } @@ -630,7 +630,7 @@ GenerateResult Qwen3Backend::generate_impl(const GenerateRequest & req, ggml_backend_get_default_buffer_type(backend_)); if (!ggml_gallocr_alloc_graph(galloc2, gf2)) { ggml_gallocr_free(galloc2); ggml_free(ectx2); - result.error = "embed2 alloc"; return result; + result.fail(GenerateErrorCode::BackendSpecific, "embed2 alloc"); return result; } ggml_backend_tensor_set(ids2, &ft, 0, sizeof(int32_t)); ggml_backend_graph_compute(backend_, gf2); @@ -639,21 +639,21 @@ GenerateResult Qwen3Backend::generate_impl(const GenerateRequest & req, ggml_free(ectx2); } if (!do_step(embed_buf.data(), 1, cur_committed, last_logits_)) { - result.error = "decode logits"; + result.fail(GenerateErrorCode::BackendSpecific, "decode logits"); return result; } cur_committed++; cache_.cur_pos = cur_committed; if (!do_decode(cur_committed, req.n_gen - 1, result.tokens, out_io)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } } } out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } @@ -665,7 +665,7 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, GenerateResult result; DaemonIO out_io = io.with_token_callback(req.on_token); if (slot < 0 || slot >= PREFIX_SLOTS || !snapshots_[slot].ctx) { - result.error = "bad slot"; + result.fail(GenerateErrorCode::InvalidSnapshotSlot); out_io.emit(-1); return result; } @@ -691,7 +691,7 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, req.prompt.end()); const int committed = do_prefill(remaining, out_io, prefix_len); if (committed < 0) { - result.error = "prefill after restore"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill after restore"); return result; } } @@ -734,7 +734,7 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, ggml_backend_get_default_buffer_type(backend_)); if (!ggml_gallocr_alloc_graph(galloc, gf)) { ggml_gallocr_free(galloc); ggml_free(ectx); - result.error = "embed alloc"; return result; + result.fail(GenerateErrorCode::BackendSpecific, "embed alloc"); return result; } ggml_backend_tensor_set(ids, &last_tok, 0, sizeof(int32_t)); ggml_backend_graph_compute(backend_, gf); @@ -745,7 +745,7 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, // Re-step at last position to get logits if (!do_step(embed_buf.data(), 1, total_committed - 1, logits)) { - result.error = "first logits"; + result.fail(GenerateErrorCode::BackendSpecific, "first logits"); return result; } @@ -765,13 +765,13 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, out_io.emit(first); if (out_io.cancelled) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } if (first == 151643 || first == 151645) { out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } @@ -796,7 +796,7 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, ggml_backend_get_default_buffer_type(backend_)); if (!ggml_gallocr_alloc_graph(galloc2, gf2)) { ggml_gallocr_free(galloc2); ggml_free(ectx2); - result.error = "embed2 alloc"; return result; + result.fail(GenerateErrorCode::BackendSpecific, "embed2 alloc"); return result; } ggml_backend_tensor_set(ids2, &ft, 0, sizeof(int32_t)); ggml_backend_graph_compute(backend_, gf2); @@ -805,21 +805,21 @@ GenerateResult Qwen3Backend::restore_and_generate_impl(int slot, ggml_free(ectx2); } if (!do_step(embed_buf.data(), 1, cur_committed, last_logits_)) { - result.error = "decode logits"; + result.fail(GenerateErrorCode::BackendSpecific, "decode logits"); return result; } cur_committed++; cache_.cur_pos = cur_committed; if (!do_decode(cur_committed, req.n_gen - 1, result.tokens, out_io)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } } } out_io.emit(-1); - result.ok = true; + result.succeed(); return result; } diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index 6869f4b3c..24ce524ae 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -798,7 +798,7 @@ GenerateResult Qwen35Backend::generate_impl(const GenerateRequest & req, auto t_prefill_start = std::chrono::steady_clock::now(); const int committed = do_prefill(req.prompt, out_io, req.snap_pos, req.snap_slot); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } auto t_prefill_end = std::chrono::steady_clock::now(); @@ -836,14 +836,14 @@ GenerateResult Qwen35Backend::generate_impl(const GenerateRequest & req, } } if (!decode_ok) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } result.decode_s = std::chrono::duration( std::chrono::steady_clock::now() - t_decode_start).count(); } - result.ok = true; + result.succeed(); return result; } @@ -855,7 +855,7 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, GenerateResult result; DaemonIO out_io = io.with_token_callback(req.on_token); if (slot < 0 || slot >= PREFIX_SLOTS || !prefix_snapshots_[slot].ctx) { - result.error = "bad slot"; + result.fail(GenerateErrorCode::InvalidSnapshotSlot); out_io.emit(-1); return result; } @@ -906,7 +906,7 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, std::vector delta = restore_prompt_delta(req.prompt, snap_pos); committed = do_prefill(delta, out_io, req.snap_pos, req.snap_slot, /*kv_offset=*/snap_pos); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } result.prefill_s = std::chrono::duration( @@ -924,7 +924,7 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, auto t_prefill_start = std::chrono::steady_clock::now(); committed = do_prefill(req.prompt, out_io, req.snap_pos, req.snap_slot); if (committed < 0) { - result.error = "prefill"; + result.fail(GenerateErrorCode::PrefillFailed); return result; } result.prefill_s = std::chrono::duration( @@ -947,7 +947,8 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, should_capture_moe_router(), /*kvflash_mask=*/pool, /*capture_qk=*/pool && kvflash_qk_policy_)) { - result.error = "restore step-graph build"; + result.fail(GenerateErrorCode::BackendSpecific, + "restore step-graph build"); return result; } } @@ -984,14 +985,14 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, } } if (!decode_ok) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } result.decode_s = std::chrono::duration( std::chrono::steady_clock::now() - t_decode_start).count(); } - result.ok = true; + result.succeed(); return result; } diff --git a/server/src/qwen35moe/qwen35moe_backend.cpp b/server/src/qwen35moe/qwen35moe_backend.cpp index 2dbd17220..d7e118934 100644 --- a/server/src/qwen35moe/qwen35moe_backend.cpp +++ b/server/src/qwen35moe/qwen35moe_backend.cpp @@ -741,7 +741,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, const DaemonIO & io) { if (!target_weights().moe_hybrid) { auto result = Qwen35Backend::generate_impl(req, io); - if (result.ok) maybe_post_request_swap(); + if (result.ok()) maybe_post_request_swap(); return result; } @@ -801,7 +801,8 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, if (!ensure_pipelined_moe_expert_compute(*pipe_state_, target_weights(), cfg_.target_path, *target_weights().moe_hybrid)) { - result.error = "moe_expert_compute_init"; + result.fail(GenerateErrorCode::BackendSpecific, + "moe_expert_compute_init"); cleanup_graphs(); return result; } @@ -861,7 +862,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, std::fprintf(stderr, "[kvflash] hybrid prompt (%d) exceeds pool %d; raise --kvflash " "or enable pflash compression\n", prompt_len, kvflash_tokens_); - result.error = "kvflash: prompt exceeds resident pool"; + result.fail(GenerateErrorCode::ContextOverflow); cleanup_graphs(); return result; } @@ -872,7 +873,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, for (int i = 0; i < prompt_len; ++i) { int32_t tok = req.prompt[(size_t)i]; if (!target_weights().embedder.embed(&tok, 1, embed_all.data() + (size_t)i * (size_t)hidden)) { - result.error = "prefill_embed"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill_embed"); cleanup_graphs(); return result; } @@ -903,7 +904,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, if (!build_layer_prefn_step(prefill_sg, target_weights(), target_cache(), target_backend(), il, /*kv_start=*/chunk_start, /*n_tokens=*/chunk_len, with_mask, /*fa_window=*/0, cfg_.kq_stride_pad)) { - result.error = "prefill_build"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill_build"); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -945,7 +946,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, // Compute batched pre-FFN auto st = ggml_backend_graph_compute(target_backend(), prefill_sg.gf); if (st != GGML_STATUS_SUCCESS) { - result.error = "prefill_compute"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill_compute"); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -969,7 +970,8 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, ? prefill_sg.moe_selected[(size_t)il] : nullptr; if (!layer_selected || !prefill_sg.moe_weights) { - result.error = "prefill_router_outputs"; + result.fail(GenerateErrorCode::BackendSpecific, + "prefill_router_outputs"); step_graph_destroy(prefill_sg); cleanup_graphs(); return result; @@ -1023,18 +1025,18 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, regions, cold_ids_copy.data(), (int)cold_ids_copy.size()); } + std::string ffn_error; bool ffn_ok = eval_moe_hybrid_ffn_batched( target_backend(), cpu_be, chunk_cfg, chunk_desc, storage, chunk_post.data(), chunk_selected.data(), chunk_weights.data(), - chunk_len, ffn_batch_out, &result.error, + chunk_len, ffn_batch_out, &ffn_error, &ffn_hot_alloc, &ffn_cold_alloc, expert_compute, expert_layer, hybrid_telemetry_ ? &ffn_tel : nullptr); if (!ffn_ok) { // Per-token fallback (avoids sm_75 mul_mat_id assertion with cold experts) - result.error.clear(); ffn_batch_out.assign((size_t)hidden * (size_t)chunk_len, 0.0f); std::vector single_out; for (int ti = 0; ti < chunk_len; ++ti) { @@ -1044,7 +1046,8 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, if (!eval_moe_hybrid_ffn_single( target_backend(), chunk_cfg, chunk_desc, storage, cpu_be, tok_post, tok_sel, tok_wts, n_expert_used, single_out)) { - result.error = "prefill_ffn_single"; + result.fail(GenerateErrorCode::BackendSpecific, + "prefill_ffn_single"); step_graph_destroy(prefill_sg); if (ffn_hot_alloc) ggml_gallocr_free(ffn_hot_alloc); if (ffn_cold_alloc) ggml_gallocr_free(ffn_cold_alloc); @@ -1175,7 +1178,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, // Get argmax from last prefill position for last_tok if (!compute_logits()) { - result.error = "decode_logits"; + result.fail(GenerateErrorCode::BackendSpecific, "decode_logits"); cleanup_graphs(); return result; } @@ -1189,20 +1192,21 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, cleanup_graphs(); if (!do_hybrid_spec_decode(committed, req.n_gen, result.tokens, out_io, &result.accept_rate)) { - result.error = "hybrid_spec_decode"; + result.fail(GenerateErrorCode::BackendSpecific, + "hybrid_spec_decode"); return result; } } else { // AR fallback decode — use pipelined path (cached DeltaNet + GPU-resident FFN) if (!ensure_pipe_state(committed)) { - result.error = "pipe_state_init"; + result.fail(GenerateErrorCode::BackendSpecific, "pipe_state_init"); cleanup_graphs(); return result; } // Get logits from last prefill token (reuses persistent logits graph) if (!compute_logits()) { - result.error = "decode_logits"; + result.fail(GenerateErrorCode::BackendSpecific, "decode_logits"); cleanup_graphs(); return result; } @@ -1231,7 +1235,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, for (int step = 1; step < req.n_gen; ++step) { int32_t tok = result.tokens.back(); if (!target_weights().embedder.embed(&tok, 1, act_cur.data())) { - result.error = "decode_embed"; + result.fail(GenerateErrorCode::BackendSpecific, "decode_embed"); cleanup_graphs(); return result; } @@ -1244,7 +1248,8 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, if (kvflash_active()) { kv_slot = kvflash_pager_.slot_for(committed); if (kv_slot < 0) { - result.error = "kvflash_slot"; + result.fail(GenerateErrorCode::BackendSpecific, + "kvflash_slot"); cleanup_graphs(); return result; } @@ -1258,7 +1263,7 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, kv_slot, /*capture_layers=*/false, routing_stats_.get())) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); cleanup_graphs(); return result; } @@ -1295,7 +1300,8 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, // act_cur stays on GPU — compute_logits reads it via GPU→GPU copy if (!compute_logits(pipe_state_->gpu_state.act_cur)) { - result.error = "decode_logits"; + result.fail(GenerateErrorCode::BackendSpecific, + "decode_logits"); cleanup_graphs(); return result; } @@ -1426,8 +1432,8 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req, ffn_tel_accum.hot_selected, ffn_tel_accum.cold_selected); std::fflush(stdout); } - result.ok = true; - if (result.ok) maybe_post_request_swap(); + result.succeed(); + if (result.ok()) maybe_post_request_swap(); return result; } @@ -1436,13 +1442,13 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, const DaemonIO & io) { if (!target_weights().moe_hybrid) { auto result = Qwen35Backend::restore_and_generate_impl(slot, req, io); - if (result.ok) maybe_post_request_swap(); + if (result.ok()) maybe_post_request_swap(); return result; } GenerateResult result; DaemonIO out_io = io.with_token_callback(req.on_token); if (slot < 0 || !snapshot_used(slot)) { - result.error = "bad slot"; + result.fail(GenerateErrorCode::InvalidSnapshotSlot); out_io.emit(-1); return result; } @@ -1469,7 +1475,7 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, } if (!restore_target_cache_from_snapshot(slot)) { - result.error = "restore"; + result.fail(GenerateErrorCode::BackendSpecific, "restore"); out_io.emit(-1); return result; } @@ -1480,7 +1486,8 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, const int prompt_len = (int)req.prompt.size(); if (prompt_len > 0 && prompt_len < snap_pos) { - result.error = "snapshot_longer_than_prompt"; + result.fail(GenerateErrorCode::BackendSpecific, + "snapshot_longer_than_prompt"); out_io.emit(-1); return result; } @@ -1493,7 +1500,7 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, std::fprintf(stderr, "[kvflash] hybrid restore prompt (%d) exceeds pool %d; raise " "--kvflash\n", prompt_len, kvflash_tokens_); - result.error = "kvflash: prompt exceeds resident pool"; + result.fail(GenerateErrorCode::ContextOverflow); out_io.emit(-1); return result; } @@ -1504,7 +1511,7 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, if (kvflash_active()) { kvflash_pager_.reset(); if (!kvflash_pager_.alloc_span(0, snap_pos)) { - result.error = "kvflash_slot"; + result.fail(GenerateErrorCode::BackendSpecific, "kvflash_slot"); out_io.emit(-1); return result; } @@ -1519,7 +1526,7 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, int32_t argmax = -1; if (!hybrid_forward_one_token(req.prompt[(size_t)i], committed, act_cur, argmax)) { - result.error = "prefill_delta"; + result.fail(GenerateErrorCode::BackendSpecific, "prefill_delta"); return result; } committed++; @@ -1549,15 +1556,15 @@ GenerateResult Qwen35MoeBackend::restore_and_generate_impl(int slot, } auto t_decode_start = std::chrono::steady_clock::now(); if (!run_pipelined_decode_path(committed, req.n_gen, result.tokens, out_io)) { - result.error = "decode"; + result.fail(GenerateErrorCode::DecodeFailed); return result; } result.decode_s = std::chrono::duration( std::chrono::steady_clock::now() - t_decode_start).count(); } - result.ok = true; - if (result.ok) maybe_post_request_swap(); + result.succeed(); + if (result.ok()) maybe_post_request_swap(); return result; } diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index 8d4bdab24..b2161ba60 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -2612,7 +2612,7 @@ void HttpServer::worker_loop() { DaemonIO scoped_io; scoped_io.stream_fd = -1; auto scoped_result = backend_.generate(scoped_req, scoped_io); - if (scoped_result.ok && backend_.snapshot_used(DISK_STAGING_SLOT)) { + if (scoped_result.ok() && backend_.snapshot_used(DISK_STAGING_SLOT)) { disk_cache_.learn_layout(DISK_STAGING_SLOT); const bool saved = disk_cache_.save(DISK_STAGING_SLOT, scoped_req.prompt); @@ -2668,7 +2668,7 @@ void HttpServer::worker_loop() { DaemonIO cold_io; cold_io.stream_fd = -1; auto cold_result = backend_.generate(cold_req, cold_io); - if (cold_result.ok && backend_.snapshot_used(DISK_STAGING_SLOT)) { + if (cold_result.ok() && backend_.snapshot_used(DISK_STAGING_SLOT)) { disk_cache_.learn_layout(DISK_STAGING_SLOT); std::vector prefix_tokens(effective_prompt.begin(), effective_prompt.begin() + cold_boundary); @@ -2940,7 +2940,7 @@ void HttpServer::worker_loop() { // Continued checkpoint: save if total tokens crossed an interval boundary. // This captures prompt + all generated tokens for long conversation reuse. if (!disk_cache_.disabled() && disk_policy.mode == DiskPrefixCacheMode::Full && - result.ok && completion_tokens > 0 && + result.ok() && completion_tokens > 0 && visible_output_seen && !client_disconnected) { int final_pos = (int)effective_prompt.size() + (int)result.tokens.size(); if (final_pos >= disk_cache_.continued_interval()) { @@ -2988,7 +2988,7 @@ void HttpServer::worker_loop() { GenTimings gen_timings{ result.prefill_s, result.decode_s }; // Record performance for /status page. - if (result.ok) { + if (result.ok()) { PerfRecord perf; perf.prompt_tokens = (int)req.prompt_tokens.size(); perf.completion_tokens = completion_tokens; @@ -3298,14 +3298,14 @@ void HttpServer::worker_loop() { result.decode_s > 0.0 ? out_tokens / result.decode_s : 0.0; const std::string finish = client_disconnected ? "client_disconnect" - : (result.ok ? emitter.finish_reason() : "error"); + : (result.ok() ? emitter.finish_reason() : "error"); std::fprintf(stderr, "[server] chat DONE %s ok=%s in=%zu effective_in=%zu out=%d " "%.1fs %.1f tok/s finish=%s restore=%s slot=%d prefix_len=%d " - "prefill=%.1fs decode=%.1fs(%.1ftok/s) error=%s\n", + "prefill=%.1fs decode=%.1fs(%.1ftok/s) error=%s detail=%s\n", req.response_id.c_str(), - result.ok ? "true" : "false", + result.ok() ? "true" : "false", req.prompt_tokens.size(), effective_prompt.size(), out_tokens, @@ -3318,7 +3318,8 @@ void HttpServer::worker_loop() { result.prefill_s, result.decode_s, decode_tok_s, - result.error.empty() ? "-" : result.error.c_str()); + result.ok() ? "-" : result.error_code().data(), + result.error_detail().empty() ? "-" : result.error_detail().data()); // Signal client thread that we're done. finish_job(); diff --git a/server/test/bench_laguna_spark.cpp b/server/test/bench_laguna_spark.cpp index 4dcb93584..858623137 100644 --- a/server/test/bench_laguna_spark.cpp +++ b/server/test/bench_laguna_spark.cpp @@ -57,8 +57,9 @@ int main(int argc, char ** argv) { DaemonIO io{}; GenerateResult r = be.generate(req, io); - if (!r.ok) { - std::fprintf(stderr, "generate failed: %s\n", r.error.c_str()); + if (!r.ok()) { + std::fprintf(stderr, "generate failed: %s (%s)\n", + r.error_code().data(), r.error_detail().data()); return 1; } const int nd = (int)r.tokens.size(); diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index 9b1dd749b..58bd272f3 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -2185,7 +2185,7 @@ static void test_layer_split_backend_inline_snapshot_and_restore_delta() { DaemonIO io; GenerateResult result = backend.generate(req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(raw->reset_called); TEST_ASSERT(raw->saved_slot == 2); TEST_ASSERT(raw->saved_pos == 3); @@ -2206,7 +2206,7 @@ static void test_layer_split_backend_inline_snapshot_and_restore_delta() { restore_req.n_gen = 1; GenerateResult restored = backend.restore_and_generate(2, restore_req, io); - TEST_ASSERT(restored.ok); + TEST_ASSERT(restored.ok()); TEST_ASSERT(raw->dflash_called); TEST_ASSERT(raw->restored_slot == 2); TEST_ASSERT(!raw->reset_called); @@ -2230,8 +2230,9 @@ static void test_layer_split_backend_sampling_capability_gate() { DaemonIO io; GenerateResult result = backend.generate(req, io); - TEST_ASSERT(!result.ok); - TEST_ASSERT(result.error == "sampling_unsupported"); + TEST_ASSERT(!result.ok()); + TEST_ASSERT(result.error->code == GenerateErrorCode::SamplingUnsupported); + TEST_ASSERT(result.error_code() == "sampling_unsupported"); } { @@ -2247,7 +2248,7 @@ static void test_layer_split_backend_sampling_capability_gate() { DaemonIO io; GenerateResult result = backend.generate(req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(result.tokens.size() == 1); TEST_ASSERT(result.tokens[0] == 12); } @@ -2264,7 +2265,7 @@ static void test_layer_split_backend_chunks_prefill_by_adapter_limit() { DaemonIO io; GenerateResult result = backend.generate(req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(raw->prefill_bases.size() == 3); TEST_ASSERT(raw->prefill_sizes.size() == 3); TEST_ASSERT(raw->prefill_bases[0] == 0); @@ -3755,7 +3756,7 @@ struct EmptySpecRetryBackend : MockBackend { const DaemonIO &) override { generate_calls++; GenerateResult result; - result.ok = true; + result.succeed(); if (req.force_ar_decode) { generate_saw_force_ar = true; result.tokens = {42}; @@ -3773,7 +3774,7 @@ struct EmptySpecRetryBackend : MockBackend { const DaemonIO &) override { restore_calls++; GenerateResult result; - result.ok = true; + result.succeed(); if (req.force_ar_decode) { restore_saw_force_ar = true; result.tokens = {84}; @@ -3797,7 +3798,7 @@ static void test_model_backend_retries_empty_spec_generate_once_with_ar() { GenerateResult result = backend.generate(req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(result.tokens.size() == 1); TEST_ASSERT(result.tokens[0] == 42); TEST_ASSERT(result.spec_decode_ran); @@ -3815,7 +3816,7 @@ static void test_model_backend_retries_empty_spec_restore_once_with_ar() { GenerateResult result = backend.restore_and_generate(7, req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(result.tokens.size() == 1); TEST_ASSERT(result.tokens[0] == 84); TEST_ASSERT(result.spec_decode_ran); @@ -3833,7 +3834,7 @@ static void test_model_backend_retries_empty_visible_spec_generate_once_with_ar( GenerateResult result = backend.generate(req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(result.tokens.size() == 1); TEST_ASSERT(result.tokens[0] == 42); TEST_ASSERT(!result.empty_visible_output); @@ -3852,7 +3853,7 @@ static void test_model_backend_retries_empty_visible_spec_restore_once_with_ar() GenerateResult result = backend.restore_and_generate(7, req, io); - TEST_ASSERT(result.ok); + TEST_ASSERT(result.ok()); TEST_ASSERT(result.tokens.size() == 1); TEST_ASSERT(result.tokens[0] == 84); TEST_ASSERT(!result.empty_visible_output); @@ -3887,7 +3888,7 @@ static void test_generate_result_accept_rate_in_usage_openai() { // Simulate the non-streaming OpenAI JSON response build. // Verify accept_rate flows from GenerateResult into usage block. GenerateResult result; - result.ok = true; + result.succeed(); result.tokens = {1, 2, 3}; result.accept_rate = 0.75f; @@ -3909,7 +3910,7 @@ static void test_generate_result_accept_rate_in_usage_openai() { static void test_generate_result_accept_rate_in_usage_anthropic() { GenerateResult result; - result.ok = true; + result.succeed(); result.tokens = {1, 2}; result.accept_rate = 0.60f; @@ -3930,11 +3931,30 @@ static void test_generate_result_accept_rate_in_usage_anthropic() { static void test_generate_result_accept_rate_zero_when_no_spec_decode() { // When spec decode doesn't run (no draft model), accept_rate stays 0. GenerateResult r; - r.ok = true; + r.succeed(); // accept_rate not set → must be 0.0f TEST_ASSERT(r.accept_rate == 0.0f); } +static void test_generate_result_error_state_is_consistent() { + GenerateResult result; + TEST_ASSERT(!result.ok()); + TEST_ASSERT(result.error->code == GenerateErrorCode::Incomplete); + TEST_ASSERT(result.error_code() == "incomplete"); + + result.fail(GenerateErrorCode::BackendSpecific, "prefill graph allocation failed"); + TEST_ASSERT(!result.ok()); + TEST_ASSERT(result.error->code == GenerateErrorCode::BackendSpecific); + TEST_ASSERT(result.error_code() == "backend_specific"); + TEST_ASSERT(result.error_detail() == "prefill graph allocation failed"); + + result.succeed(); + TEST_ASSERT(result.ok()); + TEST_ASSERT(!result.error.has_value()); + TEST_ASSERT(result.error_code().empty()); + TEST_ASSERT(result.error_detail().empty()); +} + // ═══════════════════════════════════════════════════════════════════════ // normalize_system_for_cache — header-strip tests // ═══════════════════════════════════════════════════════════════════════ @@ -4622,6 +4642,7 @@ int main() { RUN_TEST(test_generate_result_accept_rate_in_usage_openai); RUN_TEST(test_generate_result_accept_rate_in_usage_anthropic); RUN_TEST(test_generate_result_accept_rate_zero_when_no_spec_decode); + RUN_TEST(test_generate_result_error_state_is_consistent); std::fprintf(stderr, "\n── normalize_system_for_cache ──\n"); RUN_TEST(test_normalize_strips_billing_header_anthropic_array);