Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions docs/serving.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ With `C=2` and two extra Device checkpoint slots, the process owns two active St
plus a global pool of two Device-resident checkpoints. Eight pinned Host State slots and 8 GiB of
pinned Host KV retain inactive continuations under Device pressure. Active request capacity is two.

Pass `--chat-template FILE` to serve an operator-managed chat template in place of the artifact's
`frontend/chat_template.jinja`. The file replaces the template resource at startup and must be
byte-identical to a template the target recognizes; validation and acceptance are exact, so any
later edit to the file is rejected at startup. `qwen3.8-froggeric-v22.5`
(froggeric/Qwen-Fixed-Chat-Templates) is an accepted reasoning-effort template.

Other artifacts use the same command shape with their own path. For 35B-A3B DFlash, replace the MTP
selection with `--spec dflash --draft-tokens 7 --lm-head-draft`. It may remain combined with
`--vision`.
Expand Down
3 changes: 3 additions & 0 deletions include/ninfer/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ struct ContextCostOptions {

struct EngineOptions {
std::filesystem::path artifact_path;
// Optional startup replacement for the artifact's frontend/chat_template.jinja.
// The source must resolve to a template semantics the target accepts.
std::filesystem::path chat_template_path;
EnginePurpose purpose = EnginePurpose::Generation;
int device = 0;
std::uint32_t max_context = 2048; // Logical ceiling of one request or score window.
Expand Down
1 change: 1 addition & 0 deletions src/serve/generation_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ GenerationService::GenerationService(ServeOptions options, StartupObserver start
: options_(std::move(options)) {
ninfer::EngineOptions engine_options;
engine_options.artifact_path = options_.artifact_path;
engine_options.chat_template_path = options_.chat_template_path;
engine_options.device = options_.device;
engine_options.max_context = options_.max_context;
engine_options.kv_capacity = options_.kv_capacity;
Expand Down
9 changes: 8 additions & 1 deletion src/serve/serve_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ std::string serve_usage_text(const char* argv0) {
"[--response-store-max-records N] [--response-store-max-mib N] "
"[--kv-dtype bf16|int8|fp8|nvfp4|k8v4] [--spec mtp|dflash --draft-tokens N] "
"[--default-max-tokens N] [--default-thinking-budget N] "
"[--vision] [--no-cuda-graph] [--no-prefix-reuse] "
"[--vision] [--no-cuda-graph] [--no-prefix-reuse] [--chat-template FILE] "
"[--lm-head-draft] [--no-thinking] [--preserve-thinking] [--cors] "
"[--temperature F] [--top-p F] [--top-k N] [--min-p F] [--presence-penalty F] "
"[--frequency-penalty F] [--seed N] [--greedy]\n"
Expand Down Expand Up @@ -110,6 +110,8 @@ std::string serve_usage_text(const char* argv0) {
" --default-thinking-budget caps model-origin thinking for enabled requests; "
"control tokens count toward the request output limit\n"
" --preserve-thinking retains closed-turn assistant reasoning in later prompts\n"
" --chat-template FILE replaces the artifact frontend chat template at startup;\n"
" the file must be byte-identical to a template the target accepts\n"
" sampler defaults come from the loaded model and resolved thinking mode; "
"server flags and request fields override individual values.\n"
" --greedy forces temperature 0 (exact argmax).\n";
Expand Down Expand Up @@ -154,6 +156,11 @@ ServeOptions parse_serve_options(int argc, char** argv) {
if (options.model_id_override->empty()) {
throw std::invalid_argument("--model-id must not be empty");
}
} else if (arg == "--chat-template") {
options.chat_template_path = require_value("--chat-template");
if (options.chat_template_path.empty()) {
throw std::invalid_argument("--chat-template must not be empty");
}
} else if (arg == "--max-context") {
options.max_context = static_cast<std::uint32_t>(
parse_nonnegative_int(require_value("--max-context"), "max-context"));
Expand Down
2 changes: 2 additions & 0 deletions src/serve/serve_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct ServeOptions {
int port = 8080;
std::string api_key; // empty => no auth
std::optional<std::string> model_id_override; // unset => artifact identity.model_id
// Optional startup override for the artifact's frontend/chat_template.jinja.
std::filesystem::path chat_template_path;
std::string request_log_jsonl; // empty => structured request logging disabled
std::uint32_t max_context = 8192;
KvCapacityPolicy kv_capacity = KvCapacityPolicy::explicit_capacity(8192);
Expand Down
3 changes: 3 additions & 0 deletions src/targets/qwen3_6/export/ninfer/targets/qwen3_6/frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ struct FrontendOptions {
std::size_t media_cache_bytes = kDefaultMediaCacheBytes;
std::size_t media_live_bytes = kDefaultMediaLiveBytes;
std::uint32_t media_preprocess_threads = 0;
// Optional startup replacement for resources.chat_template_jinja. An empty path keeps
// the artifact template. The replacement must resolve to an accepted template semantics.
std::filesystem::path chat_template_path;
};

struct FrontendResources;
Expand Down
15 changes: 15 additions & 0 deletions src/targets/qwen3_6/impl/frontend/chat_template.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ constexpr Sha256Digest kReasoningEffortTemplateDigest{
0xd3, 0xe2, 0xa7, 0x25, 0xb6, 0xc2, 0x58, 0x6a, 0xaa, 0x3a, 0x8a, 0xf9, 0xd7, 0xa8, 0x10, 0x41,
};

// froggeric/Qwen-Fixed-Chat-Templates v22.5 (template_version "qwen3.8-froggeric-v22.5").
// Renders the same ChatML surface as the reasoning-effort template above. The fixed C++
// renderer does not implement this family's in-message control markers (<|im_start|>think_on|>,
// <|im_start|>think_off|>, <|im_start|>think_low|> embedded in message content), the
// auto_disable_thinking_with_tools knob, or max_tool_arg_chars/max_tool_response_chars
// truncation; requests relying on those render with the semantics above. Acceptance is
// byte-exact; any edit to the template file changes the digest and is rejected.
constexpr Sha256Digest kReasoningEffortTemplateV225Digest{
0xe5, 0x76, 0x84, 0xba, 0xe4, 0x15, 0x62, 0x11, 0xa5, 0x54, 0x73, 0xc5, 0xa6, 0x3b, 0xe9, 0x76,
0xa4, 0x05, 0xa3, 0x7a, 0xb5, 0xbe, 0x5a, 0xe0, 0xe5, 0xab, 0xf1, 0xdf, 0x53, 0x49, 0xc4, 0xb2,
};

constexpr std::string_view kLowReasoningInstructions =
"Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to "
"the conclusion without unnecessary elaboration.";
Expand Down Expand Up @@ -419,6 +431,9 @@ CompiledChatTemplate CompiledChatTemplate::resolve(std::string_view source) {
if (digest == kReasoningEffortTemplateDigest) {
return CompiledChatTemplate(ChatTemplateSemantics::ReasoningEffort);
}
if (digest == kReasoningEffortTemplateV225Digest) {
return CompiledChatTemplate(ChatTemplateSemantics::ReasoningEffort);
Comment on lines +434 to +435

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Give v22.5 its own medium-default semantics

When --chat-template selects v22.5, thinking is enabled, and the client omits reasoning_effort, the accepted source explicitly defaults to medium (reasoning_effort_froggeric_v225_chat_template.jinja:17-29), but this branch aliases it to semantics whose capabilities and renderer default to XHigh (capabilities() at line 448 and resolve_reasoning_instructions() at line 367). Ordinary requests therefore receive an extra xhigh system instruction and are logged as xhigh instead of rendering the selected template; represent the v22.5 default and render behavior separately rather than aliasing it to the existing template.

Useful? React with 👍 / 👎.

}
throw std::invalid_argument("unsupported frontend/chat_template.jinja (sha256 " +
sha256_hex(digest) + ")");
}
Expand Down
44 changes: 44 additions & 0 deletions src/targets/qwen3_6/impl/frontend/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <limits>
#include <memory>
#include <optional>
#include <sstream>
#include <span>
#include <stdexcept>
#include <string>
Expand Down Expand Up @@ -230,6 +232,38 @@ fi::CompiledChatTemplate compile_chat_template(const FrontendResources& resource
return fi::CompiledChatTemplate::resolve(resources.chat_template_jinja);
}

// Replaces the artifact chat template with an operator-provided source and keeps
// tokenizer_config.json.chat_template byte-consistent with it, so the existing
// validation and template-resolution gates run against the override unchanged.
void apply_chat_template_override(FrontendResources& resources,
const std::filesystem::path& chat_template_path) {
std::ifstream stream(chat_template_path, std::ios::binary);
if (!stream) {
throw std::invalid_argument("chat template file is not readable: " +
chat_template_path.string());
}
std::ostringstream buffer;
buffer << stream.rdbuf();
std::string source = buffer.str();
if (source.size() > (10U << 20)) {
Comment on lines +245 to +248

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the size cap before buffering the file

When FILE is accidentally a very large file or an unbounded stream, the entire input is copied into the ostringstream and then again into source before the 10 MiB check runs. Pointing this option at a multi-gigabyte artifact can therefore exhaust memory or stall startup instead of producing the intended size-limit error; bound the read to 10 MiB plus one byte, optionally using an early regular-file size check.

Useful? React with 👍 / 👎.

throw std::invalid_argument("chat template file exceeds 10 MiB: " +
chat_template_path.string());
}
// A single trailing newline is not template content: the jinja source parser
// drops it, and the acceptance digest is defined over the content without it.
if (!source.empty() && source.back() == '\n') { source.pop_back(); }
resources.chat_template_jinja = std::move(source);
Json tokenizer_config =
parse_resource_json(resources.tokenizer_config_json, "tokenizer_config.json");
tokenizer_config["chat_template"] = resources.chat_template_jinja;
try {
resources.tokenizer_config_json = tokenizer_config.dump();
} catch (const nlohmann::json::exception&) {
throw std::invalid_argument("chat template file is not valid UTF-8 text: " +
chat_template_path.string());
}
}

[[noreturn]] void throw_processor_error(const fi::ProcessorError& error) {
switch (error.kind()) {
case fi::ProcessorErrorKind::BudgetExceeded:
Expand Down Expand Up @@ -1355,6 +1389,11 @@ Frontend& Frontend::operator=(Frontend&&) noexcept = default;
Frontend::~Frontend() = default;

Frontend make_frontend(const FrontendResources& resources, FrontendOptions options) {
if (!options.chat_template_path.empty()) {
FrontendResources overridden = resources;
apply_chat_template_override(overridden, options.chat_template_path);
return Frontend(std::make_shared<const Frontend::Impl>(overridden, true, options));
}
return Frontend(std::make_shared<const Frontend::Impl>(resources, true, options));
}

Expand All @@ -1368,6 +1407,11 @@ Frontend FrontendTestAccess::create_component(const FrontendResources& resources

Frontend FrontendTestAccess::create_component(const FrontendResources& resources,
FrontendOptions options) {
if (!options.chat_template_path.empty()) {
FrontendResources overridden = resources;
apply_chat_template_override(overridden, options.chat_template_path);
return Frontend(std::make_shared<const Frontend::Impl>(overridden, false, options));
}
return Frontend(std::make_shared<const Frontend::Impl>(resources, false, options));
}

Expand Down
1 change: 1 addition & 0 deletions src/targets/qwen3_6_27b/impl/package.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ Package::Frontend Package::make_frontend(const LoadedModel& model, const EngineO
.media_cache_bytes = options.media_cache_bytes,
.media_live_bytes = options.media_live_bytes,
.media_preprocess_threads = options.media_preprocess_threads,
.chat_template_path = options.chat_template_path,
});
}

Expand Down
1 change: 1 addition & 0 deletions src/targets/qwen3_6_35b_a3b/impl/package.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Package::Frontend Package::make_frontend(const LoadedModel& model, const EngineO
.media_cache_bytes = options.media_cache_bytes,
.media_live_bytes = options.media_live_bytes,
.media_preprocess_threads = options.media_preprocess_threads,
.chat_template_path = options.chat_template_path,
});
}

Expand Down
Loading