Skip to content

Commit 29476d1

Browse files
add NLA llama-server endpoints
Assisted by: pi (model: codex 5.5)
1 parent 4d05d7d commit 29476d1

10 files changed

Lines changed: 1007 additions & 0 deletions

File tree

‎convert_hf_to_gguf.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3858,7 +3858,54 @@ def set_gguf_parameters(self):
38583858
super().set_gguf_parameters()
38593859
self._try_set_pooling_type()
38603860

3861+
nla_meta = self._nla_meta()
3862+
if nla_meta is not None and (self.dir_model / "value_head.safetensors").is_file():
3863+
self.gguf_writer.add_string("nla.role", "critic")
3864+
self.gguf_writer.add_uint32("nla.extraction_layer", int(nla_meta.get("extraction_layer_index", nla_meta.get("critic", {}).get("extraction_layer_index", 20))))
3865+
mse_scale = nla_meta.get("extraction", {}).get("mse_scale", 59.86651818838306)
3866+
self.gguf_writer.add_float32("nla.mse_scale", float(mse_scale))
3867+
ar_template = nla_meta.get("prompt_templates", {}).get("ar")
3868+
if ar_template:
3869+
self.gguf_writer.add_string("nla.prompt_template.ar", str(ar_template))
3870+
3871+
def _nla_meta(self) -> dict[str, Any] | None:
3872+
meta_path = self.dir_model / "nla_meta.yaml"
3873+
if not meta_path.is_file():
3874+
return None
3875+
try:
3876+
import yaml # type: ignore[import-not-found]
3877+
with open(meta_path, "r", encoding="utf-8") as f:
3878+
data = yaml.safe_load(f)
3879+
return data if isinstance(data, dict) else {}
3880+
except Exception as e:
3881+
logger.warning("failed to parse nla_meta.yaml: %s", e)
3882+
return {}
3883+
3884+
def _is_nla_critic(self) -> bool:
3885+
return (self.dir_model / "value_head.safetensors").is_file()
3886+
3887+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
3888+
yield from super().generate_extra_tensors()
3889+
if not self._is_nla_critic():
3890+
return
3891+
3892+
if "model.norm.weight" not in self.model_tensors:
3893+
n_embd = int(self.hparams["hidden_size"])
3894+
yield self.format_tensor_name(gguf.MODEL_TENSOR.OUTPUT_NORM), torch.ones(n_embd, dtype=torch.float32)
3895+
3896+
from safetensors.torch import load_file
3897+
value_head = load_file(str(self.dir_model / "value_head.safetensors"))["weight"]
3898+
yield "nla.value_head.weight", value_head
3899+
3900+
def tensor_force_quant(self, name: str, new_name: str, bid: int | None, n_dims: int) -> gguf.GGMLQuantizationType | bool:
3901+
if new_name == "nla.value_head.weight":
3902+
return gguf.GGMLQuantizationType.BF16
3903+
return super().tensor_force_quant(name, new_name, bid, n_dims)
3904+
38613905
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
3906+
if name.startswith("nla."):
3907+
yield name, data_torch
3908+
return
38623909
if self.hf_arch == "Qwen2Model":
38633910
name = f"model.{name}" # map to Qwen2ForCausalLM tensors
38643911
yield from super().modify_tensors(data_torch, name, bid)

‎examples/CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ else()
1818
add_subdirectory(debug)
1919
add_subdirectory(embedding)
2020
add_subdirectory(eval-callback)
21+
add_subdirectory(extract-layer)
22+
add_subdirectory(nla-generate)
2123

2224
add_subdirectory(gguf-hash)
2325
add_subdirectory(gguf)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
set(TARGET extract-layer)
2+
add_executable(${TARGET} extract-layer.cpp)
3+
install(TARGETS ${TARGET} RUNTIME)
4+
target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT})
5+
target_compile_features(${TARGET} PRIVATE cxx_std_17)
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// extract-layer.cpp — Extract residual-stream activations at a specific layer
2+
// from a GGUF model using llama.cpp's eval callback mechanism.
3+
//
4+
// Usage:
5+
// extract-layer -m model.gguf -p "prompt text" --layer 20
6+
//
7+
// Outputs a summary to stderr and writes the raw fp32 activation vector
8+
// for the last token position to the file specified by --out (or stdout).
9+
10+
#include "arg.h"
11+
#include "common.h"
12+
#include "log.h"
13+
#include "llama.h"
14+
15+
#include <cassert>
16+
#include <cmath>
17+
#include <cstdio>
18+
#include <cstring>
19+
#include <fstream>
20+
#include <string>
21+
#include <vector>
22+
23+
// Capture state for the callback
24+
struct capture_state {
25+
int32_t target_layer;
26+
int32_t l_out_count = 0;
27+
int64_t n_embd = 0;
28+
int64_t n_tokens = 0;
29+
std::vector<float> captured;
30+
bool found = false;
31+
};
32+
33+
static bool extract_cb(struct ggml_tensor * t, bool ask, void * user_data) {
34+
auto * st = (capture_state *) user_data;
35+
36+
if (ask) {
37+
return true; // always compute
38+
}
39+
40+
std::string name(t->name);
41+
42+
// Log every tensor name for debugging (only first decode)
43+
static int log_count = 0;
44+
if (log_count < 5) {
45+
LOG_INF("cb: name='%s' ne=[%lld,%lld]\n",
46+
t->name, (long long)t->ne[0], (long long)t->ne[1]);
47+
log_count++;
48+
}
49+
50+
// Per-layer residual stream output is named "l_out-<layer_idx>"
51+
// e.g. "l_out-0", "l_out-1", ..., "l_out-27" for Qwen2.5-7B (28 layers)
52+
std::string target_name = "l_out-" + std::to_string(st->target_layer);
53+
if (name == target_name) {
54+
int64_t n_el = ggml_nelements(t);
55+
LOG_INF("CAPTURED layer %d: %lld elements, n_tokens=%lld\n",
56+
st->target_layer, (long long)n_el, (long long)t->ne[1]);
57+
st->captured.resize(n_el);
58+
st->n_tokens = t->ne[1];
59+
60+
const bool is_host = ggml_backend_buffer_is_host(t->buffer);
61+
if (is_host) {
62+
for (int64_t i = 0; i < n_el; i++) {
63+
st->captured[i] = ggml_get_f32_1d(t, i);
64+
}
65+
} else {
66+
auto n_bytes = ggml_nbytes(t);
67+
std::vector<uint8_t> buf(n_bytes);
68+
ggml_backend_tensor_get(t, buf.data(), 0, n_bytes);
69+
ggml_tensor tmp = *t;
70+
tmp.data = buf.data();
71+
tmp.buffer = nullptr;
72+
for (int64_t i = 0; i < n_el; i++) {
73+
st->captured[i] = ggml_get_f32_1d(&tmp, i);
74+
}
75+
}
76+
st->found = true;
77+
st->l_out_count++;
78+
}
79+
80+
return true;
81+
}
82+
83+
int main(int argc, char ** argv) {
84+
std::setlocale(LC_NUMERIC, "C");
85+
86+
common_params params;
87+
params.prompt = "Hello, how are you?";
88+
89+
// Custom args — strip our custom flags before common_params_parse sees them
90+
int32_t target_layer = 20;
91+
std::string output_file;
92+
std::vector<char *> filtered_argv;
93+
filtered_argv.push_back(argv[0]);
94+
for (int i = 1; i < argc; i++) {
95+
std::string arg(argv[i]);
96+
if (arg == "--layer" && i + 1 < argc) {
97+
target_layer = std::atoi(argv[++i]);
98+
} else if (arg == "--out" && i + 1 < argc) {
99+
output_file = argv[++i];
100+
} else {
101+
filtered_argv.push_back(argv[i]);
102+
}
103+
}
104+
105+
// Parse common params (handles -m, -p, -ngl, etc.)
106+
if (!common_params_parse((int)filtered_argv.size(), filtered_argv.data(), params, LLAMA_EXAMPLE_COMMON)) {
107+
return 1;
108+
}
109+
110+
// CPU only (VRAM occupied)
111+
params.n_gpu_layers = 0;
112+
113+
// Set up capture callback
114+
capture_state state;
115+
state.target_layer = target_layer;
116+
117+
params.cb_eval = extract_cb;
118+
params.cb_eval_user_data = &state;
119+
params.warmup = false; // skip warmup to avoid triggering callback
120+
121+
llama_backend_init();
122+
llama_numa_init(params.numa);
123+
124+
auto llama_init = common_init_from_params(params);
125+
auto * model = llama_init->model();
126+
auto * ctx = llama_init->context();
127+
128+
if (!model || !ctx) {
129+
LOG_ERR("%s: failed to init model/context\n", __func__);
130+
return 1;
131+
}
132+
133+
state.n_embd = llama_model_n_embd(model);
134+
135+
// Tokenize
136+
const llama_vocab * vocab = llama_model_get_vocab(model);
137+
const bool add_bos = llama_vocab_get_add_bos(vocab);
138+
std::vector<llama_token> tokens = common_tokenize(ctx, params.prompt, add_bos, true);
139+
140+
if (tokens.empty()) {
141+
LOG_ERR("%s: no input tokens\n", __func__);
142+
return 1;
143+
}
144+
145+
LOG_INF("%s: %zu tokens, extracting layer %d, d_model=%d\n",
146+
__func__, tokens.size(), target_layer, (int)state.n_embd);
147+
148+
// Enable embeddings so the graph includes the embedding output path
149+
llama_set_embeddings(ctx, true);
150+
151+
llama_batch batch = llama_batch_get_one(tokens.data(), tokens.size());
152+
if (batch.logits) {
153+
batch.logits[batch.n_tokens - 1] = true;
154+
}
155+
156+
int ret = llama_decode(ctx, batch);
157+
if (ret != 0) {
158+
LOG_ERR("%s: llama_decode failed (%d)\n", __func__, ret);
159+
return 1;
160+
}
161+
162+
if (!state.found) {
163+
LOG_ERR("%s: failed to capture layer %d (saw %d l_out tensors)\n",
164+
__func__, target_layer, state.l_out_count);
165+
return 1;
166+
}
167+
168+
// Extract last token position
169+
int64_t pos = state.n_tokens - 1;
170+
std::vector<float> activation(state.n_embd);
171+
// ggml layout: [n_embd, n_tokens] — column-major in ggml terms,
172+
// but ggml_get_f32_1d linearizes, so element (i, pos) = pos*n_embd + i
173+
for (int64_t i = 0; i < state.n_embd; i++) {
174+
activation[i] = state.captured[pos * state.n_embd + i];
175+
}
176+
177+
// L2 norm
178+
float l2 = 0.0f;
179+
for (auto v : activation) l2 += v * v;
180+
l2 = std::sqrt(l2);
181+
182+
// Write output
183+
if (!output_file.empty()) {
184+
std::ofstream ofs(output_file, std::ios::binary);
185+
ofs.write(reinterpret_cast<const char *>(activation.data()),
186+
activation.size() * sizeof(float));
187+
ofs.close();
188+
LOG_INF("Wrote %zu floats to %s\n", activation.size(), output_file.c_str());
189+
}
190+
191+
// Summary to stderr
192+
LOG("\n=== Layer %d Activation (last token, pos=%lld) ===\n", target_layer, (long long)pos);
193+
LOG("Prompt: %s\n", params.prompt.c_str());
194+
LOG("Tokens: %zu total\n", tokens.size());
195+
LOG("d_model: %d\n", (int)state.n_embd);
196+
LOG("Vector: %zu floats (%zu bytes)\n", activation.size(), activation.size() * sizeof(float));
197+
LOG("L2 norm: %.4f\n", l2);
198+
LOG("First 8: ");
199+
for (int i = 0; i < (int)std::min((size_t)8, activation.size()); i++) {
200+
LOG("%.6f ", activation[i]);
201+
}
202+
LOG("\n");
203+
LOG("Model layers: %d, l_out count: %d\n",
204+
llama_model_n_layer(model), state.l_out_count);
205+
206+
llama_backend_free();
207+
return 0;
208+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
set(TARGET nla-generate)
2+
add_executable(${TARGET} nla-generate.cpp)
3+
install(TARGETS ${TARGET} RUNTIME)
4+
target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT})
5+
target_compile_features(${TARGET} PRIVATE cxx_std_17)

0 commit comments

Comments
 (0)