Skip to content
Merged
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
54 changes: 49 additions & 5 deletions server/src/common/gguf_inspect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,36 @@

namespace dflash::common {

bool derive_effective_target_layer_count(const std::string & arch,
uint32_t block_count,
uint32_t nextn_predict_layers,
uint32_t & target_layer_count,
std::string & error) {
target_layer_count = block_count;
error.clear();

if (block_count == 0) {
error = "block_count must be greater than zero";
return false;
}

// Embedded NextN blocks are currently defined by the Qwen3.5/3.6 GGUF
// layout. Do not reinterpret similarly named metadata on other arches.
if (arch != "qwen35" && arch != "qwen35moe") {
return true;
}
if (nextn_predict_layers == 0) {
return true;
}
if (nextn_predict_layers >= block_count) {
error = "nextn_predict_layers must be smaller than block_count";
return false;
}

target_layer_count = block_count - nextn_predict_layers;
return true;
}

GgufModelInfo inspect_gguf_model_info(const char * path) {
GgufModelInfo info;

Expand All @@ -35,12 +65,26 @@ GgufModelInfo inspect_gguf_model_info(const char * path) {
if (v) info.name = v;
}

// Read layer count: <arch>.block_count
// Read target layer count. Qwen3.5/3.6 GGUFs can include trailing
// embedded MTP blocks in block_count.
if (!info.arch.empty()) {
std::string key = info.arch + ".block_count";
int64_t kid = gguf_find_key(gctx, key.c_str());
if (kid >= 0) {
info.n_layer = (int)gguf_get_val_u32(gctx, kid);
const std::string block_key = info.arch + ".block_count";
const int64_t block_id = gguf_find_key(gctx, block_key.c_str());
if (block_id >= 0) {
const uint32_t block_count = gguf_get_val_u32(gctx, block_id);

const std::string nextn_key = info.arch + ".nextn_predict_layers";
const int64_t nextn_id = gguf_find_key(gctx, nextn_key.c_str());
const uint32_t nextn = nextn_id >= 0
? gguf_get_val_u32(gctx, nextn_id)
: 0;

uint32_t target_layers = 0;
std::string error;
if (derive_effective_target_layer_count(
info.arch, block_count, nextn, target_layers, error)) {
info.n_layer = (int)target_layers;
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion server/src/common/gguf_inspect.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ namespace dflash::common {
struct GgufModelInfo {
std::string arch; // e.g. "qwen35", "laguna", "qwen3", "gemma4"
std::string name; // optional general.name display string
int n_layer = -1;
int n_layer = -1; // target layers used for inference
};

// Read architecture, display name, and layer count from a GGUF file.
// Returns info with arch="" and n_layer=-1 on failure.
GgufModelInfo inspect_gguf_model_info(const char * path);

// Derive the inference target layer count from GGUF metadata. Qwen3.5/3.6
// checkpoints may append embedded NextN/MTP predictor blocks after the target
// transformer. Those blocks are not target layers and must not participate in
// target placement or full-attention interval validation.
bool derive_effective_target_layer_count(const std::string & arch,
uint32_t block_count,
uint32_t nextn_predict_layers,
uint32_t & target_layer_count,
std::string & error);

// Richer GGUF identity captured at server startup and re-emitted at /props.
// All header values are best-effort: missing keys leave the corresponding
// field at the listed default (empty string or -1). `ok` is false only if
Expand Down
70 changes: 66 additions & 4 deletions server/src/qwen35/gguf_target_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

#include "internal.h"
#include "common/derived_scalars.h"
#include "common/gguf_inspect.h"
#include "common/layer_split_utils.h"
#include "common/gguf_mmap.h"
#include "common/gguf_bounds.h"
Expand Down Expand Up @@ -175,6 +176,36 @@ static bool should_load_target_tensor(const char * name,
return false;
}

static bool validate_embedded_nextn_blocks(const gguf_context * gctx,
uint32_t target_layer_count,
uint32_t block_count,
std::string & err) {
if (target_layer_count == block_count) return true;

const int64_t n_tensors = gguf_get_n_tensors(gctx);
for (uint32_t il = target_layer_count; il < block_count; ++il) {
const std::string prefix = "blk." + std::to_string(il) + ".nextn.";
bool found_nextn_tensor = false;
for (int64_t tid = 0; tid < n_tensors; ++tid) {
const char * name = gguf_get_tensor_name(gctx, tid);
if (name && std::strncmp(name, prefix.c_str(), prefix.size()) == 0) {
found_nextn_tensor = true;
break;
}
}
if (!found_nextn_tensor) {
char buf[256];
std::snprintf(
buf, sizeof(buf),
"GGUF declares embedded NextN block %u but has no tensor with prefix '%s'",
il, prefix.c_str());
err = buf;
return false;
}
}
return true;
}

struct TargetTensorAlloc {
ggml_tensor * tensor = nullptr;
size_t file_offset = 0;
Expand Down Expand Up @@ -329,7 +360,32 @@ bool load_target_gguf_partial(const std::string & path,

const uint32_t n_embd = get_u32_or(gctx, key("embedding_length").c_str(), 0);
const uint32_t n_ff = get_u32_or(gctx, key("feed_forward_length").c_str(), 0);
const uint32_t n_layer = get_u32_or(gctx, key("block_count").c_str(), 0);
const uint32_t block_count = get_u32_or(gctx, key("block_count").c_str(), 0);
const uint32_t nextn_predict_layers =
get_u32_or(gctx, key("nextn_predict_layers").c_str(), 0);
uint32_t n_layer = 0;
if (!derive_effective_target_layer_count(
arch_str, block_count, nextn_predict_layers, n_layer, err)) {
set_last_error("invalid target layer metadata: " + err);
ggml_free(meta_ctx);
gguf_free(gctx);
return false;
}
if (!validate_embedded_nextn_blocks(gctx, n_layer, block_count, err)) {
set_last_error(err);
ggml_free(meta_ctx);
gguf_free(gctx);
return false;
}
if (nextn_predict_layers > 0) {
std::fprintf(
stderr,
"[loader] ignoring %u embedded NextN/MTP block%s: "
"target_layers=%u total_blocks=%u\n",
nextn_predict_layers,
nextn_predict_layers == 1 ? "" : "s",
n_layer, block_count);
}
const uint32_t n_head = get_u32_or(gctx, key("attention.head_count").c_str(), 0);
const uint32_t n_headkv= get_u32_or(gctx, key("attention.head_count_kv").c_str(), 0);
const uint32_t kl = get_u32_or(gctx, key("attention.key_length").c_str(), 0);
Expand Down Expand Up @@ -361,10 +417,12 @@ bool load_target_gguf_partial(const std::string & path,
if (invalid_common || invalid_dense || invalid_moe) {
char buf[512];
std::snprintf(buf, sizeof(buf),
"invalid %s hparams: n_embd=%u n_layer=%u n_head=%u n_head_kv=%u "
"invalid %s hparams: n_embd=%u n_layer=%u block_count=%u "
"nextn=%u n_head=%u n_head_kv=%u "
"kl=%u vl=%u n_ff=%u n_ff_exp=%u n_ff_shexp=%u n_expert=%u used=%u "
"fai=%u ssm{conv=%u inner=%u state=%u dt=%u grp=%u}",
arch_str.c_str(), n_embd, n_layer, n_head, n_headkv, kl, vl, n_ff,
arch_str.c_str(), n_embd, n_layer, block_count,
nextn_predict_layers, n_head, n_headkv, kl, vl, n_ff,
n_ff_exp, n_ff_shexp, n_expert, n_expert_used,
fai, ssm_conv, ssm_inner, ssm_state, ssm_dt, ssm_grp);
set_last_error(buf);
Expand All @@ -379,7 +437,11 @@ bool load_target_gguf_partial(const std::string & path,
}
if (n_layer % fai != 0) {
char buf[128];
std::snprintf(buf, sizeof(buf), "block_count=%u not divisible by full_attention_interval=%u", n_layer, fai);
std::snprintf(
buf, sizeof(buf),
"target_layer_count=%u not divisible by full_attention_interval=%u "
"(block_count=%u nextn_predict_layers=%u)",
n_layer, fai, block_count, nextn_predict_layers);
set_last_error(buf);
gguf_free(gctx); return false;
}
Expand Down
31 changes: 31 additions & 0 deletions server/test/test_server_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "common/kvflash_pager.h"
#include "placement/draft_residency.h"
#include "common/gguf_bounds.h"
#include "common/gguf_inspect.h"
#include "ggml-cpu.h"
#include "server/prompt_normalize.h"
#include "qwen3_drafter_model.h"
Expand Down Expand Up @@ -5422,3 +5423,33 @@ TEST_CASE(ServerUnitFixture, test_gguf_bounds_error_reports_operands) {
kMax, 10, 10, 100);
TEST_ASSERT(o.find("overflow") != std::string::npos);
}

TEST_CASE(ServerUnitFixture, test_qwen35_embedded_mtp_target_layer_count) {
uint32_t target_layers = 0;
std::string error;

TEST_ASSERT(derive_effective_target_layer_count(
"qwen35", 64, 0, target_layers, error));
TEST_ASSERT(target_layers == 64);

TEST_ASSERT(derive_effective_target_layer_count(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"qwen35", 65, 1, target_layers, error));
TEST_ASSERT(target_layers == 64);

TEST_ASSERT(derive_effective_target_layer_count(
"qwen35moe", 81, 1, target_layers, error));
TEST_ASSERT(target_layers == 80);

TEST_ASSERT(!derive_effective_target_layer_count(
"qwen35", 1, 1, target_layers, error));
TEST_ASSERT(error.find("smaller than block_count") != std::string::npos);

TEST_ASSERT(!derive_effective_target_layer_count(
"qwen35", 0, 0, target_layers, error));
TEST_ASSERT(error.find("greater than zero") != std::string::npos);

// Do not reinterpret similarly named metadata for unrelated architectures.
TEST_ASSERT(derive_effective_target_layer_count(
"laguna", 65, 1, target_layers, error));
TEST_ASSERT(target_layers == 65);
}
Loading