Skip to content

Commit 920ea56

Browse files
committed
server: make prompt cache state swaps atomic
1 parent 9836a57 commit 920ea56

4 files changed

Lines changed: 386 additions & 116 deletions

File tree

tests/test-server-prompt-cache.cpp

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
static server_prompt make_prompt(
99
int64_t n_tokens,
1010
std::initializer_list<int64_t> checkpoints,
11-
size_t checkpoint_size = 0) {
11+
size_t checkpoint_size = 0,
12+
llama_token token = 1) {
1213
server_prompt prompt {
13-
server_tokens(llama_tokens(n_tokens, 1), false),
14+
server_tokens(llama_tokens(n_tokens, token), false),
1415
{},
1516
};
1617

@@ -23,6 +24,30 @@ static server_prompt make_prompt(
2324
return prompt;
2425
}
2526

27+
static bool cache_contains(
28+
const server_prompt_cache & cache,
29+
const server_prompt_cache_state * state) {
30+
for (const auto & cur : cache.states) {
31+
if (&cur == state) {
32+
return true;
33+
}
34+
}
35+
36+
return false;
37+
}
38+
39+
static bool cache_contains_token(
40+
const server_prompt_cache & cache,
41+
llama_token token) {
42+
for (const auto & cur : cache.states) {
43+
if (!cur.prompt.tokens.empty() && cur.prompt.tokens[0] == token) {
44+
return true;
45+
}
46+
}
47+
48+
return false;
49+
}
50+
2651
int main() {
2752
{
2853
auto prompt = make_prompt(20000, {4096, 12000, 18000});
@@ -102,5 +127,78 @@ int main() {
102127
assert(state->prompt.checkpoints.size() == prompt.checkpoints.size());
103128
}
104129

130+
{
131+
server_prompt_cache cache(4, 10000);
132+
auto live = make_prompt(1000, {100});
133+
auto cached = make_prompt(900, {});
134+
server_tokens target(llama_tokens(950, 1), false);
135+
136+
auto * cached_state = cache.alloc(cached, 512*1024, 0);
137+
auto * selected = cache.find_better(live, target, true, 1);
138+
139+
assert(selected == cached_state);
140+
}
141+
142+
{
143+
constexpr size_t mib = 1024*1024;
144+
145+
server_prompt_cache cache(1, 10000);
146+
auto cached = make_prompt(900, {}, 0, 1);
147+
auto live = make_prompt(1000, {}, 0, 2);
148+
server_tokens target(llama_tokens(950, 1), false);
149+
150+
auto * cached_state = cache.alloc(cached, 512*1024, 0);
151+
auto * selected = cache.find_better(live, target, true, 1);
152+
assert(selected == cached_state);
153+
154+
auto * saved_state = cache.alloc(live, 768*1024, 0);
155+
assert(cache.finalize(saved_state, &target, true, selected));
156+
157+
// The selected entry is pinned during the swap. The cache may exceed
158+
// its steady-state budget until that entry is restored and removed.
159+
assert(saved_state != nullptr);
160+
assert(cache_contains(cache, cached_state));
161+
assert(cache_contains(cache, saved_state));
162+
assert(cache.size() == 1280*1024);
163+
assert(cache.size() > mib);
164+
}
165+
166+
{
167+
server_prompt_cache cache(2, 10000);
168+
auto useful = make_prompt(600, {}, 0, 1);
169+
auto useless = make_prompt(600, {}, 0, 2);
170+
auto current = make_prompt(600, {}, 0, 3);
171+
server_tokens target(llama_tokens(700, 1), false);
172+
173+
auto * useful_state = cache.alloc(useful, 512*1024, 0);
174+
auto * useless_state = cache.alloc(useless, 512*1024, 0);
175+
assert(useful_state != nullptr);
176+
assert(useless_state != nullptr);
177+
178+
auto * current_state = cache.alloc(current, 1280*1024, 0);
179+
assert(cache.finalize(current_state, &target, false));
180+
181+
assert(current_state != nullptr);
182+
assert(cache_contains(cache, useful_state));
183+
assert(!cache_contains_token(cache, 2));
184+
assert(cache_contains(cache, current_state));
185+
}
186+
187+
{
188+
server_prompt_cache cache(4, 10000);
189+
auto live = make_prompt(1000, {322});
190+
auto cached = make_prompt(900, {322});
191+
192+
llama_tokens target_tokens(950, 1);
193+
target_tokens[3] = 2;
194+
server_tokens target(target_tokens, false);
195+
196+
cache.alloc(cached, 512*1024, 0);
197+
198+
// No exact recurrent state exists before the divergence at token 3.
199+
assert(live.reusable_prefix_tokens(3, target.size(), true) == 0);
200+
assert(cache.find_better(live, target, true, 1) == nullptr);
201+
}
202+
105203
return 0;
106204
}

tools/server/server-context.cpp

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,13 @@ struct server_slot {
217217

218218
server_prompt prompt;
219219

220-
bool prompt_save(server_prompt_cache & prompt_cache) const {
220+
server_prompt_cache_state * prompt_save(
221+
server_prompt_cache & prompt_cache,
222+
const server_tokens * tokens_new = nullptr,
223+
bool state_exact = false,
224+
const server_prompt_cache_state * state_protected = nullptr) const {
221225
if (prompt.tokens.size() == 0) {
222-
return false;
226+
return nullptr;
223227
}
224228

225229
const size_t cur_size_tgt = llama_state_seq_get_size_ext(ctx_tgt, id, LLAMA_STATE_SEQ_FLAGS_NONE);
@@ -232,23 +236,46 @@ struct server_slot {
232236

233237
auto * cur = prompt_cache.alloc(prompt, cur_size_tgt, cur_size_dft);
234238
if (cur == nullptr) {
235-
return false;
239+
return nullptr;
240+
}
241+
242+
const size_t n_tgt = llama_state_seq_get_data_ext(
243+
ctx_tgt,
244+
cur->data.main.data(),
245+
cur_size_tgt,
246+
id,
247+
LLAMA_STATE_SEQ_FLAGS_NONE);
248+
if (n_tgt != cur_size_tgt) {
249+
SLT_ERR(*this, "failed to save main prompt state: expected %zu bytes, wrote %zu\n", cur_size_tgt, n_tgt);
250+
prompt_cache.discard(cur);
251+
return nullptr;
236252
}
237253

238-
llama_state_seq_get_data_ext(ctx_tgt, cur->data.main.data(), cur_size_tgt, id, LLAMA_STATE_SEQ_FLAGS_NONE);
239254
if (ctx_dft) {
240-
llama_state_seq_get_data_ext(ctx_dft, cur->data.drft.data(), cur_size_dft, id, LLAMA_STATE_SEQ_FLAGS_NONE);
255+
const size_t n_dft = llama_state_seq_get_data_ext(
256+
ctx_dft,
257+
cur->data.drft.data(),
258+
cur_size_dft,
259+
id,
260+
LLAMA_STATE_SEQ_FLAGS_NONE);
261+
if (n_dft != cur_size_dft) {
262+
SLT_ERR(*this, "failed to save draft prompt state: expected %zu bytes, wrote %zu\n", cur_size_dft, n_dft);
263+
prompt_cache.discard(cur);
264+
return nullptr;
265+
}
241266
}
242267

243-
return true;
268+
if (!prompt_cache.finalize(cur, tokens_new, state_exact, state_protected)) {
269+
return nullptr;
270+
}
271+
272+
return cur;
244273
}
245274

246275
bool prompt_load(
247276
server_prompt_cache & prompt_cache,
248-
const server_tokens & tokens,
249-
bool state_exact,
250-
int64_t min_reuse_gain) {
251-
bool res = prompt_cache.load(prompt, tokens, ctx_tgt, ctx_dft, id, state_exact, min_reuse_gain);
277+
server_prompt_cache_state * state) {
278+
bool res = prompt_cache.load(prompt, state, ctx_tgt, ctx_dft, id);
252279
if (!res) {
253280
SLT_WRN(*this, "%s", "failed to load prompt from cache\n");
254281
}
@@ -1619,21 +1646,51 @@ struct server_context_impl {
16191646
}
16201647

16211648
if (ret) {
1622-
update_cache = update_cache && prompt_cache;
1649+
server_prompt_cache_state * state_selected = nullptr;
1650+
bool state_exact = false;
16231651

1624-
// cache prompts only for completion tasks
1625-
update_cache = update_cache && task.type == SERVER_TASK_TYPE_COMPLETION;
1652+
const bool can_update_cache =
1653+
prompt_cache &&
1654+
task.type == SERVER_TASK_TYPE_COMPLETION;
1655+
1656+
if (can_update_cache) {
1657+
state_exact = ctx_tgt_state_exact();
1658+
const int64_t min_reuse_gain = std::max<int64_t>(1, params_base.checkpoint_min_step / 2);
1659+
state_selected = prompt_cache->find_better(
1660+
ret->prompt,
1661+
task.tokens,
1662+
state_exact,
1663+
min_reuse_gain);
1664+
1665+
const int lcp_live = ret->prompt.tokens.get_common_prefix(task.tokens);
1666+
const bool extends_live =
1667+
!ret->prompt.tokens.empty() &&
1668+
lcp_live == ret->prompt.n_tokens();
1669+
1670+
// A direct append can continue in the live slot without a host
1671+
// round-trip. A better saved branch still requires a state swap.
1672+
update_cache = state_selected != nullptr || (update_cache && !extends_live);
1673+
} else {
1674+
update_cache = false;
1675+
}
16261676

16271677
if (update_cache) {
16281678
SRV_TRC("%s", "updating prompt cache\n");
16291679

16301680
const int64_t t_start = ggml_time_us();
16311681

1632-
ret->prompt_save(*prompt_cache);
1633-
1634-
const int64_t min_reuse_gain = std::max<int64_t>(1, params_base.checkpoint_min_step / 2);
1635-
if (!ret->prompt_load(*prompt_cache, task.tokens, ctx_tgt_state_exact(), min_reuse_gain)) {
1636-
ret->prompt_clear();
1682+
auto * state_saved = ret->prompt_save(
1683+
*prompt_cache,
1684+
&task.tokens,
1685+
state_exact,
1686+
state_selected);
1687+
1688+
if (!ret->prompt_load(*prompt_cache, state_selected)) {
1689+
// The active state was serialized before the switch. Restore
1690+
// it if the selected cache entry could not be loaded.
1691+
if (state_saved == nullptr || !ret->prompt_load(*prompt_cache, state_saved)) {
1692+
ret->prompt_clear();
1693+
}
16371694
}
16381695

16391696
prompt_cache->update();

0 commit comments

Comments
 (0)