Entropic 2.11.1
Local-first agentic inference engine
Loading...
Searching...
No Matches
speculative_compat.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
15
16#include <llama.h>
17
18#include <algorithm>
19#include <cstring>
20#include <optional>
21#include <string>
22
23namespace entropic::speculative {
24
25namespace {
26
27// Mirrors llama.cpp's constants. Keep in sync with
28// extern/llama.cpp/common/speculative.cpp if upstream tightens.
29constexpr int kSpecVocabMaxSizeDifference = 128;
30constexpr int kSpecVocabCheckStartTokenId = 5;
31
59std::optional<std::string> check_arch_gate(
60 const llama_model* target) {
61 if (llama_model_is_recurrent(target)) {
62 return std::string{
63 "target model is recurrent (Mamba/RWKV) — speculative "
64 "decoding is incompatible with recurrent architectures "
65 "at the v2.1.11 llama.cpp pin"};
66 }
67 if (llama_model_is_hybrid(target)) {
68 return std::string{
69 "target model is hybrid (e.g., QWEN35/QWEN35MOE, "
70 "NEMOTRON_H, JAMBA, GRANITE_HYBRID) — speculative "
71 "decoding produces divergent state at split-prefill "
72 "boundaries on hybrid SSM architectures at the v2.1.11 "
73 "llama.cpp pin (see proposal Implementation Log, Gate A)"};
74 }
75 return std::nullopt;
76}
77
87std::optional<std::string> check_vocab_type(
88 const llama_vocab* vt, const llama_vocab* vd) {
89 if (llama_vocab_type(vt) != llama_vocab_type(vd)) {
90 return std::string{
91 "vocab type differs between target and draft models"};
92 }
93 return std::nullopt;
94}
95
105std::optional<std::string> check_bos(
106 const llama_vocab* vt, const llama_vocab* vd) {
107 const bool add_t = llama_vocab_get_add_bos(vt);
108 const bool add_d = llama_vocab_get_add_bos(vd);
109 if (add_t != add_d) {
110 return std::string{"BOS add-behavior differs"};
111 }
112 if (add_t && llama_vocab_bos(vt) != llama_vocab_bos(vd)) {
113 return std::string{"BOS token id differs"};
114 }
115 return std::nullopt;
116}
117
127std::optional<std::string> check_eos(
128 const llama_vocab* vt, const llama_vocab* vd) {
129 const bool add_t = llama_vocab_get_add_eos(vt);
130 const bool add_d = llama_vocab_get_add_eos(vd);
131 if (add_t != add_d) {
132 return std::string{"EOS add-behavior differs"};
133 }
134 if (add_t && llama_vocab_eos(vt) != llama_vocab_eos(vd)) {
135 return std::string{"EOS token id differs"};
136 }
137 return std::nullopt;
138}
139
150std::optional<std::string> check_vocab_size(
151 const llama_vocab* vt, const llama_vocab* vd) {
152 const int nt = llama_vocab_n_tokens(vt);
153 const int nd = llama_vocab_n_tokens(vd);
154 const int diff = (nt > nd) ? (nt - nd) : (nd - nt);
155 if (diff > kSpecVocabMaxSizeDifference) {
156 return std::string{
157 "vocab size difference exceeds the speculative tolerance "
158 "(target=" + std::to_string(nt)
159 + ", draft=" + std::to_string(nd)
160 + ", max-allowed-diff="
161 + std::to_string(kSpecVocabMaxSizeDifference) + ")"};
162 }
163 return std::nullopt;
164}
165
179std::optional<std::string> check_token_text(
180 const llama_vocab* vt, const llama_vocab* vd) {
181 const int nt = llama_vocab_n_tokens(vt);
182 const int nd = llama_vocab_n_tokens(vd);
183 const int end = std::min(nt, nd);
184 for (int i = kSpecVocabCheckStartTokenId; i < end; ++i) {
185 const char* tt = llama_vocab_get_text(vt, i);
186 const char* td = llama_vocab_get_text(vd, i);
187 if (tt == nullptr || td == nullptr || std::strcmp(tt, td) != 0) {
188 return std::string{
189 "token text differs at id "
190 + std::to_string(i)
191 + " — draft tokenizer is not a prefix-compatible "
192 "subset of the target"};
193 }
194 }
195 return std::nullopt;
196}
197
198} // anonymous namespace
199
200namespace {
201
216std::string run_vocab_checks(
217 const llama_vocab* vt, const llama_vocab* vd) {
218 using Check = std::optional<std::string> (*)(
219 const llama_vocab*, const llama_vocab*);
220 static constexpr Check checks[] = {
221 &check_vocab_type, &check_bos, &check_eos,
222 &check_vocab_size, &check_token_text,
223 };
224 std::string err;
225 for (Check fn : checks) {
226 if (err.empty()) {
227 auto d = fn(vt, vd);
228 if (d.has_value()) { err = std::move(*d); }
229 }
230 }
231 return err;
232}
233
246std::string build_compat_diagnostic(
247 const llama_model* target, const llama_model* draft) {
248 std::string err;
249 if (target == nullptr || draft == nullptr) {
250 err = "null model handle (target or draft)";
251 } else if (auto d = check_arch_gate(target); d.has_value()) {
252 err = std::move(*d);
253 } else {
254 const llama_vocab* vt = llama_model_get_vocab(target);
255 const llama_vocab* vd = llama_model_get_vocab(draft);
256 err = (vt == nullptr || vd == nullptr)
257 ? std::string{"model vocab unavailable (target or draft)"}
258 : run_vocab_checks(vt, vd);
259 }
260 return err;
261}
262
263} // anonymous namespace
264
280 const llama_model* target, const llama_model* draft) {
281 std::string err = build_compat_diagnostic(target, draft);
282 return CompatResult{err.empty(), std::move(err)};
283}
284
285} // namespace entropic::speculative
Tokenizer/architecture compatibility check for speculative decoding draft pairing.
CompatResult check_compat(const llama_model *target, const llama_model *draft)
Check whether a draft model can pair with a target for sequential speculative decoding.
Result of a draft/target compatibility check.