Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
bebc388
fix(serve): a turn past the device KV cache is a 400 "context exceeds…
noahgift Sep 28, 2026
167bd5e
roadmap: PMAT-4616 (D5): the ticket #4614 implements, not #3715
noahgift Sep 28, 2026
9243b42
evidence(PMAT-4616): pr-review receipt for 167bd5e126 (reviewer sonne…
noahgift Sep 28, 2026
099d07d
evidence(PMAT-4616): commit the CI signer's signature for the 167bd5e…
noahgift Sep 28, 2026
b96392c
fix(cuda): chunk the dense batched prefill; QK-norm models stop takin…
noahgift Sep 28, 2026
1c4b84c
fix(chat): size the chat device KV to the context when VRAM allows (#…
noahgift Sep 28, 2026
81a7908
fix(cuda): Qwen3.5 prefill dequantizes F16 and IQ4_XS on the device (…
noahgift Sep 28, 2026
fdc165f
fix(cuda): a QK-norm model keeps the serial prefill when its FP16 bat…
noahgift Sep 28, 2026
18b6c83
evidence(pr-review): #4620 non-author receipt for fdc165f50d (DEGRADE…
noahgift Sep 29, 2026
65370ad
test(cuda): the fit-check no-op cases assert the path of the profile …
noahgift Sep 29, 2026
926edb2
fix(PMAT-4616): D5 gate reds — the cap is CUDA-free and tested, try_c…
noahgift Sep 29, 2026
76555e8
evidence(pr-review): #4620 non-author receipt for 65370adc9a (DEGRADE…
noahgift Sep 29, 2026
2b8d79a
evidence(pr-review): drop the superseded fdc165f50d receipt from #462…
noahgift Sep 29, 2026
2d43ad4
docs(audits): quorum PMAT-4621 AGREED 3/3 (sonnet-5, gemini-3.1-pro-h…
noahgift Sep 29, 2026
e3a7890
evidence(pr-review): stamp #4620's receipt with the diff patch-id Arm…
noahgift Sep 29, 2026
015b0c9
fix(PMAT-4616): CUDA /generate and /batch/generate map ContextLimitEx…
noahgift Sep 29, 2026
1ead14b
merge origin/main — roadmap.yaml regenerated with make roadmap-aggregate
noahgift Sep 29, 2026
b953e8f
evidence(pr-review): #4620 receipt passes the whole guard, not only t…
noahgift Sep 29, 2026
7008062
fix(PMAT-4616): every serve generate/logprobs/perplexity error routes…
noahgift Sep 29, 2026
23f058b
split(#4621): #4620 carries roots 1+5 only (chunked dense prefill, F1…
noahgift Sep 29, 2026
cd324d0
fix(#4621): QK-norm models take the FP16 batched prefill when it fits…
noahgift Sep 29, 2026
7da9a62
fix(#4621): apr chat sizes its device KV to the context (root 3)
noahgift Sep 29, 2026
b3e1e46
evidence(PMAT-4616): pr-review receipt for 7008062734 (reviewer sonne…
noahgift Sep 29, 2026
595b014
evidence(PMAT-4616): bind the 7008062734 receipt to diff patch-id 2fe…
noahgift Sep 29, 2026
bd46408
evidence(PMAT-4616): 7008062734 receipt — crux block and citations ma…
noahgift Sep 29, 2026
66c07c4
fix(#4621): a chunked prefill's later chunks never replay the prefill…
noahgift Sep 29, 2026
31cd3f6
evidence(pr-review): drop #4620's pre-split receipt (it reviewed root…
noahgift Sep 29, 2026
6562916
evidence(pr-review): #4620 Arm-4 receipt for split head 31cd3f6c40 (P…
noahgift Sep 29, 2026
451bc77
merge ec/3715-lambda-cells (A @6562916bee: PREFILL_GRAPH chunk fix) i…
noahgift Sep 29, 2026
f61c073
merge B (ec/4621-fit-and-chat-kv @451bc77c52) into C
noahgift Sep 29, 2026
91e70ca
evidence(pr-review): #4620 post-split quorum (3/3 AGREED, scope A) + …
noahgift Sep 29, 2026
1c97bea
merge ec/3715-lambda-cells (A @91e70ca127: A-scope quorum json) into B
noahgift Sep 29, 2026
f3bf4ae
docs(audits): quorum PMAT-4621 AGREED 3/3 (sonnet-5, gemini-3.1-pro-h…
noahgift Sep 29, 2026
42477d4
merge B (ec/4621-fit-and-chat-kv @f3bf4ae869) into C
noahgift Sep 29, 2026
2cab59f
fix(serve): K1 — size the GPU KV to the model's context, not a fixed …
noahgift Sep 29, 2026
98abc8a
test(serve): K1 — the 24 GB 1.7B serve budget includes the batched slots
noahgift Sep 29, 2026
0c0e824
merge #4614 (2e/d5-serve-kv-4xx) into K1 — R2: use its serving-contex…
noahgift Sep 29, 2026
bc201cc
refactor(serve): K1 adds serving_kv_len beside #4623's session_kv_len…
noahgift Sep 29, 2026
b77bd07
test(serve): K1 — at the limit 200 whole, past it a 400, on the devic…
noahgift Sep 29, 2026
1683fa1
fix(serve): size the GPU KV from the model's context; refuse past it …
noahgift Sep 29, 2026
067f906
merge devont/k1-kv-only: #4630 becomes the KV-only vehicle on main (c…
noahgift Sep 29, 2026
5d983db
ci: first main-based run for #4630 (retarget does not fire pull_request)
noahgift Sep 29, 2026
6a6a1c2
merge main: pick up #4632 wasmtime 48.0.3 (RUSTSEC-2026-0315/0316) fo…
noahgift Sep 29, 2026
45d616e
docs(roadmap): PMAT-4636 K1 entry (#4636); PMAT-4616 AC1 names the se…
noahgift Sep 29, 2026
8df8d6d
docs(audits): quorum PMAT-4616,PMAT-4636 AGREED 3/3 at 45d616e6e7 (#4…
noahgift Sep 29, 2026
44cb53e
docs(pr-review): #4630 receipt at 8df8d6d0a7 (DEGRADED, independent s…
noahgift Sep 29, 2026
d593b5c
merge origin/main into ec/4621-chat-session-kv
noahgift Oct 4, 2026
af59e3c
fix(serve): request tools never reached the chat template; Qwen3.5 XM…
noahgift Oct 4, 2026
5a008ac
fix(serve): pass tools=None at the dense_session_4268 test call site …
noahgift Oct 4, 2026
da4f4f9
merge devont/k1-serve-kv (#4630) onto main (316dee2cd4)
noahgift Oct 4, 2026
56ad4d4
evidence(pr-review): receipt for #4623 at d593b5c99b
noahgift Oct 4, 2026
3cc3d7a
fold: #4630 into #4623 -- serve sizes its GPU KV to the model context…
noahgift Oct 4, 2026
616ff4c
evidence(pr-review): receipt for #4623 at 3cc3d7a71b
noahgift Oct 4, 2026
728d1a2
fix(guards): aggregate the PMAT-4650 fragment and re-point a moved ci…
noahgift Oct 5, 2026
26ccc7f
refactor(serve): extract-fn try_cuda_backend and completions_inner un…
noahgift Oct 5, 2026
6a83be3
Merge remote-tracking branch 'origin/main' into w4711/fix-4650
noahgift Oct 5, 2026
b777d5c
Merge remote-tracking branch 'origin/ec/4621-chat-session-kv' into w4…
noahgift Oct 5, 2026
942e3f8
style(serve): rustfmt two include!d files the include-fmt ratchet fla…
noahgift Oct 5, 2026
be139e0
evidence(pr-review): #4711 receipt at 942e3f8fe6 (reviewer claude-son…
noahgift Oct 5, 2026
68b2819
test(serve): K1 never passes as a skip; #[ignore] + panic on CUDA ini…
noahgift Oct 5, 2026
e95438e
test(serve): K1 runs in cuda-unit again; drop #[ignore], keep the pan…
noahgift Oct 5, 2026
717cd9b
evidence(pr-review): #4711 receipt at e95438e370 (reviewer claude-son…
noahgift Oct 5, 2026
f70cf5a
fix(serve): parse Qwen XML tool calls before JSON when the text has <…
noahgift Oct 5, 2026
0b6096d
fix(serve): with <function= in the text the Qwen XML result is final
noahgift Oct 5, 2026
e63bc04
evidence(pr-review): #4711 receipt at 0b6096d752 (reviewer claude-son…
noahgift Oct 5, 2026
f233825
test(serve): request tools reach the GGUF's own template (kills reali…
noahgift Oct 5, 2026
ae9d0ee
evidence(pr-review): #4711 receipt at f2338256ee (reviewer claude-son…
noahgift Oct 5, 2026
b49a662
revert: unfold #4623/#4630 serve KV from #4711; it lands on its own
noahgift Oct 5, 2026
25f354a
merge origin/main into #4711 (BEHIND; merge-only, L17)
noahgift Oct 6, 2026
66a8f04
evidence(pr-review): #4711 receipt at 25f354a206 (reviewer claude-son…
noahgift Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 151 additions & 96 deletions crates/aprender-serve/src/api/cuda_chat_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ fn try_safetensors_cuda_backend(

// #4007: the loaded model's architecture, not the client's `model` string.
// #3723: the request's thinking mode; an ON the template cannot express is refused by name.
let prompt = match crate::api::realize_handlers::format_chat_messages_for_state_thinking(
let prompt = match crate::api::realize_handlers::format_chat_messages_for_state_thinking_tools(
state,
&request.messages,
Some(&request.model),
request.thinking(),
request.tools.as_deref(),
) {
Ok(p) => p,
Err(e) => return Some(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())),
Expand Down Expand Up @@ -121,7 +122,7 @@ async fn try_cuda_backend(
// D5: refused before any path (streaming, batch scheduler, direct) takes the
// model, so the client gets a 400 rather than a 500 or a mid-stream error.
let tokenized =
tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state);
tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state);
let prompt_ids =
match fit_serving_context(state, tokenized) {
Ok(ids) => ids,
Expand Down Expand Up @@ -152,49 +153,10 @@ async fn try_cuda_backend(
let (timing_tx, timing_rx) =
tokio::sync::oneshot::channel::<crate::api::PhaseTimings>();

// PMAT-044: Use batch scheduler if available (continuous batching)
if let Some(batch_tx) = state.cuda_batch_tx() {
let batch_req = super::cuda_batch_scheduler::CudaBatchRequest {
prompt_ids,
config: q_config,
token_tx: tx,
non_streaming: false,
enqueue_time: std::time::Instant::now(),
timing_tx: Some(timing_tx),
};
if let Err(e) = batch_tx.try_send(batch_req) {
// §5.2: this 503 is the one admission REFUSAL this server has
// (the policy is otherwise `queue`), so it is counted where it
// is returned — `kv.admission_rejected` reads this counter.
state.record_admission_rejected();
return Some(fail_response(
state,
StatusCode::SERVICE_UNAVAILABLE,
format!("Batch queue full: {e}"),
));
}
} else {
// Fallback: direct RwLock path (serialized)
let cuda_model_clone = cuda_model_lock.clone();
let prompt_ids_clone = prompt_ids.clone();
let q_config_clone = q_config.clone();
let sink_metrics = state.metrics.clone();

tokio::task::spawn_blocking(move || {
let mut cuda_model = cuda_model_clone.write().expect("operation failed");
let generate_start = std::time::Instant::now();
// Stops when the client goes away — see `streaming_token_sink`.
let sink =
crate::api::openai_handlers::streaming_token_sink(tx.clone(), sink_metrics);
let result =
dense_cuda_turn(&mut cuda_model, &prompt_ids_clone, &q_config_clone, sink);
// Taken under the SAME write lock the request ran under, so the
// split belongs to this request and to no other.
let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start));
if let Err(e) = result {
let _ = tx.blocking_send(Err(e.to_string()));
}
});
if let Err(r) =
dispatch_cuda_stream(state, cuda_model_lock, prompt_ids, q_config, tx, timing_tx)
{
return Some(r);
}

return Some(true_streaming_sse_response(
Expand All @@ -213,54 +175,14 @@ async fn try_cuda_backend(

// Non-streaming CUDA — route through batch scheduler when available (realizr#211)
let (timing_tx, timing_rx) = tokio::sync::oneshot::channel::<crate::api::PhaseTimings>();
let (token_ids, completion_tokens, response_text) = if let Some(batch_tx) = state.cuda_batch_tx() {
// Use batch scheduler: submit request and collect all tokens
// realizr#212: capacity 512 for bulk-send after non-streaming generation
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<u32, String>>(512);
let batch_req = super::cuda_batch_scheduler::CudaBatchRequest {
prompt_ids,
config: q_config,
token_tx: tx,
non_streaming: true, // realizr#212: scheduler accumulates + bulk-sends
enqueue_time: std::time::Instant::now(),
timing_tx: Some(timing_tx),
};
if let Err(e) = batch_tx.try_send(batch_req) {
// §5.2: counted at the refusal, as above.
state.record_admission_rejected();
return Some(fail_response(
state,
StatusCode::SERVICE_UNAVAILABLE,
format!("Batch queue full: {e}"),
));
}
// Collect all tokens via async receive (realizr#211)
let mut tokens = Vec::new();
while let Some(result) = rx.recv().await {
match result {
Ok(token_id) => tokens.push(token_id),
// D5: `fit_serving_context` above already refused an over-context
// prompt with a 400; the scheduler's errors are strings, and what
// reaches here is a server fault.
Err(e) => return Some(fail_response(state, StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
let n = tokens.len();
let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new());
(tokens, n, clean_chat_output(&text))
let generated = if let Some(batch_tx) = state.cuda_batch_tx() {
cuda_batch_collect(state, batch_tx, prompt_ids, q_config, timing_tx, &tokenizer).await
} else {
// Fallback: direct RwLock path (serialized, no batch scheduler)
let mut cuda_model = cuda_model_lock.write().expect("operation failed");
let generate_start = std::time::Instant::now();
let generated = match dense_cuda_turn(&mut cuda_model, &prompt_ids, &q_config, |_| true) {
Ok(g) => g,
Err(e) => return Some(fail_response(state, crate::api::generation_error_status(&e), e)),
};
let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start));
let tokens: Vec<u32> = generated.iter().skip(prompt_tokens).copied().collect();
let n = tokens.len();
let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new());
(tokens, n, clean_chat_output(&text))
cuda_direct_collect(state, cuda_model_lock, &prompt_ids, &q_config, timing_tx, &tokenizer, prompt_tokens)
};
let (token_ids, completion_tokens, response_text) = match generated {
Ok(g) => g,
Err(r) => return Some(r),
};

let latency = start.elapsed();
Expand All @@ -287,6 +209,137 @@ async fn try_cuda_backend(
))
}

/// `try_cuda_backend`'s streaming dispatch: the batch scheduler when one is
/// running, else the direct RwLock path on a blocking task. `Err` is the 503
/// for a full batch queue.
#[cfg(feature = "cuda")]
#[allow(clippy::result_large_err)]
fn dispatch_cuda_stream(
state: &AppState,
cuda_model_lock: &Arc<std::sync::RwLock<crate::gguf::OwnedQuantizedModelCuda>>,
prompt_ids: Vec<u32>,
q_config: crate::gguf::QuantizedGenerateConfig,
tx: tokio::sync::mpsc::Sender<Result<u32, String>>,
timing_tx: tokio::sync::oneshot::Sender<crate::api::PhaseTimings>,
) -> Result<(), Response> {
// PMAT-044: Use batch scheduler if available (continuous batching)
if let Some(batch_tx) = state.cuda_batch_tx() {
let batch_req = super::cuda_batch_scheduler::CudaBatchRequest {
prompt_ids,
config: q_config,
token_tx: tx,
non_streaming: false,
enqueue_time: std::time::Instant::now(),
timing_tx: Some(timing_tx),
};
if let Err(e) = batch_tx.try_send(batch_req) {
// §5.2: this 503 is the one admission REFUSAL this server has
// (the policy is otherwise `queue`), so it is counted where it
// is returned — `kv.admission_rejected` reads this counter.
state.record_admission_rejected();
return Err(fail_response(
state,
StatusCode::SERVICE_UNAVAILABLE,
format!("Batch queue full: {e}"),
));
}
} else {
// Fallback: direct RwLock path (serialized)
let cuda_model_clone = cuda_model_lock.clone();
let sink_metrics = state.metrics.clone();

tokio::task::spawn_blocking(move || {
let mut cuda_model = cuda_model_clone.write().expect("operation failed");
let generate_start = std::time::Instant::now();
// Stops when the client goes away — see `streaming_token_sink`.
let sink =
crate::api::openai_handlers::streaming_token_sink(tx.clone(), sink_metrics);
let result = dense_cuda_turn(&mut cuda_model, &prompt_ids, &q_config, sink);
// Taken under the SAME write lock the request ran under, so the
// split belongs to this request and to no other.
let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start));
if let Err(e) = result {
let _ = tx.blocking_send(Err(e.to_string()));
}
});
}
Ok(())
}

/// `try_cuda_backend`'s non-streaming turn through the batch scheduler
/// (realizr#211): submit, then collect every token. Returns
/// `(tokens, completion_tokens, cleaned text)`.
#[cfg(feature = "cuda")]
async fn cuda_batch_collect(
state: &AppState,
batch_tx: &tokio::sync::mpsc::Sender<super::cuda_batch_scheduler::CudaBatchRequest>,
prompt_ids: Vec<u32>,
q_config: crate::gguf::QuantizedGenerateConfig,
timing_tx: tokio::sync::oneshot::Sender<crate::api::PhaseTimings>,
tokenizer: &BPETokenizer,
) -> Result<(Vec<u32>, usize, String), Response> {
// Use batch scheduler: submit request and collect all tokens
// realizr#212: capacity 512 for bulk-send after non-streaming generation
let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<u32, String>>(512);
let batch_req = super::cuda_batch_scheduler::CudaBatchRequest {
prompt_ids,
config: q_config,
token_tx: tx,
non_streaming: true, // realizr#212: scheduler accumulates + bulk-sends
enqueue_time: std::time::Instant::now(),
timing_tx: Some(timing_tx),
};
if let Err(e) = batch_tx.try_send(batch_req) {
// §5.2: counted at the refusal, as in `dispatch_cuda_stream`.
state.record_admission_rejected();
return Err(fail_response(
state,
StatusCode::SERVICE_UNAVAILABLE,
format!("Batch queue full: {e}"),
));
}
// Collect all tokens via async receive (realizr#211)
let mut tokens = Vec::new();
while let Some(result) = rx.recv().await {
match result {
Ok(token_id) => tokens.push(token_id),
// D5: `fit_serving_context` already refused an over-context
// prompt with a 400; the scheduler's errors are strings, and what
// reaches here is a server fault.
Err(e) => return Err(fail_response(state, StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
let n = tokens.len();
let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new());
Ok((tokens, n, clean_chat_output(&text)))
}

/// `try_cuda_backend`'s non-streaming turn when no batch scheduler runs: the
/// direct RwLock path (serialized). Same return shape as `cuda_batch_collect`.
#[cfg(feature = "cuda")]
#[allow(clippy::result_large_err)]
fn cuda_direct_collect(
state: &AppState,
cuda_model_lock: &Arc<std::sync::RwLock<crate::gguf::OwnedQuantizedModelCuda>>,
prompt_ids: &[u32],
q_config: &crate::gguf::QuantizedGenerateConfig,
timing_tx: tokio::sync::oneshot::Sender<crate::api::PhaseTimings>,
tokenizer: &BPETokenizer,
prompt_tokens: usize,
) -> Result<(Vec<u32>, usize, String), Response> {
let mut cuda_model = cuda_model_lock.write().expect("operation failed");
let generate_start = std::time::Instant::now();
let generated = match dense_cuda_turn(&mut cuda_model, prompt_ids, q_config, |_| true) {
Ok(g) => g,
Err(e) => return Err(fail_response(state, crate::api::generation_error_status(&e), e)),
};
let _ = timing_tx.send(phase_split(&mut cuda_model, generate_start));
let tokens: Vec<u32> = generated.iter().skip(prompt_tokens).copied().collect();
let n = tokens.len();
let text = tokenizer.decode(&tokens).unwrap_or_else(|_| String::new());
Ok((tokens, n, clean_chat_output(&text)))
}

/// §3: pair the engine's prefill measurement with the decode remainder.
///
/// `take_phase_timings` clears as it reads, so a request that took a
Expand Down Expand Up @@ -358,7 +411,7 @@ fn try_quantized_backend(
// GH-319: Use actual model architecture for chat template detection
let arch_hint = state.model_architecture();
let prompt_ids =
match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state) {
match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state) {
Ok(ids) => ids,
Err(r) => return Some(r),
};
Expand Down Expand Up @@ -516,7 +569,7 @@ fn try_apr_transformer_backend(
};
let arch_hint = state.model_architecture();
let prompt_ids =
match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state) {
match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state) {
Ok(ids) => ids,
Err(r) => return Some(r),
};
Expand Down Expand Up @@ -613,11 +666,12 @@ fn registry_prompt_ids(
request: &ChatCompletionRequest,
tokenizer: &BPETokenizer,
) -> Result<Vec<u32>, Response> {
let prompt_text = match crate::api::realize_handlers::format_chat_messages_for_state_thinking(
let prompt_text = match crate::api::realize_handlers::format_chat_messages_for_state_thinking_tools(
state,
&request.messages,
Some(&request.model),
request.thinking(),
request.tools.as_deref(),
) {
Ok(p) => p,
Err(e) => return Err(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())),
Expand Down Expand Up @@ -820,7 +874,7 @@ async fn try_apr_q4k_chat_backend(
};
let arch_hint = state.model_architecture();
let prompt_ids =
match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), state) {
match tokenize_chat_prompt(&tokenizer, &request.messages, arch_hint.as_deref(), request.thinking(), request.tools.as_deref(), state) {
Ok(ids) => ids,
Err(r) => return Some(r),
};
Expand Down Expand Up @@ -1229,6 +1283,7 @@ fn try_qwen3_moe_backend(
&request.messages,
Some(&request.model),
request.thinking(),
request.tools.as_deref(),
state,
) {
Ok(ids) => ids,
Expand Down
44 changes: 29 additions & 15 deletions crates/aprender-serve/src/api/gpu_completions_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,10 +598,33 @@ async fn completions_inner(
return Ok(r);
}

// GH-627/637/670: Direct CUDA model fallback for APR GPU path
// with_cuda_model_and_vocab sets cuda_model but NOT model,
// so registry_completions fails with "No model available".
#[cfg(feature = "cuda")]
if let Some(r) = try_cuda_direct_completions(&state, &request, max_tokens, temperature, start)? {
return Ok(r);
}

// aprender#2609: the f32 APR / SafeTensors CPU backend, in the same position
// the chat chain puts it — after quantized, before the dense registry.
if let Some(r) =
try_apr_transformer_completions(&state, &request, max_tokens, temperature, start, &cancel)?
{
return Ok(r);
}

registry_completions(&state, &request, max_tokens, temperature, start, &cancel)
}

/// GH-627/637/670: Direct CUDA model fallback for APR GPU path.
/// with_cuda_model_and_vocab sets cuda_model but NOT model,
/// so registry_completions fails with "No model available".
#[cfg(feature = "cuda")]
fn try_cuda_direct_completions(
state: &AppState,
request: &CompletionRequest,
max_tokens: usize,
temperature: f32,
start: std::time::Instant,
) -> Result<Option<CompletionResponse>, RErr> {
if let Some(cuda_lock) = state.cuda_model() {
use crate::gguf::QuantizedGenerateConfig;
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
Expand Down Expand Up @@ -636,7 +659,7 @@ async fn completions_inner(
// #2465(2): this inline backend ignored `request.stop` too.
let (text, finish_reason) =
apply_stop_sequences(text, request.stop.as_deref(), completion_tokens, max_tokens);
return Ok(CompletionResponse {
return Ok(Some(CompletionResponse {
id: format!("cmpl-cuda-{}", elapsed.as_millis()),
object: "text_completion".to_string(),
created: std::time::SystemTime::now()
Expand All @@ -657,18 +680,9 @@ async fn completions_inner(
},
// The inline CUDA block records no backend flag either (#3894).
used_gpu: None,
});
}));
}

// aprender#2609: the f32 APR / SafeTensors CPU backend, in the same position
// the chat chain puts it — after quantized, before the dense registry.
if let Some(r) =
try_apr_transformer_completions(&state, &request, max_tokens, temperature, start, &cancel)?
{
return Ok(r);
}

registry_completions(&state, &request, max_tokens, temperature, start, &cancel)
Ok(None)
}

/// The `/v1/logprobs` generation config: greedy, logprobs on, and stopping on
Expand Down
Loading
Loading