Pull requests / #3565

#3565 [Engine] serve: auto sessions, an opt-in disk parking around a conversation switch

open · @drumblund · 0 コメント · GitHub で見る

Server & APIAMD / HIPNVIDIA / CUDAModels & quants

本文

<h2>Problem and result</h2><p>The engine has held a saved conversation across restarts for a while: <code>--slot-save-path</code> plus
<code>POST /slots/0?action=save|restore</code> write and read a session file, and <code>SAVE &lt;path&gt;</code> / <code>RESTORE &lt;path&gt;</code> on
the engine's control stream does the work. What was missing is anything that <em>calls</em> it.</p><p>So a single-slot service had exactly one way to survive a conversation switch: the in-RAM cache, bounded by
<code>--conversation-cache-mib</code>. That bound is easy to hit — at 512K a 27k-token conversation parks 651 MB, so a
24 GiB ceiling holds roughly thirty of them and then starts evicting. And a restart, or a conversation that
was evicted, pays a full re-read of a prompt that can be minutes of prefill.</p><p>After, with <code>"auto_sessions": true</code>:</p>
  | before | after
-- | -- | --
switch back to a conversation the RAM cache had dropped | full re-read | restored from its file
27k-token conversation, 512K config | — | saved 27,174 tokens (0.65 GB) in 223 ms, restored in 146 ms
normal service stop | held conversation lost | saved before the stop, restored by the next start

<p>The restored answer matches the one given before the switch character for character.</p><h2>Related discussion</h2><p>none — no issue was opened for this. Off by default (<code>auto_sessions</code> is false unless the config says
otherwise), and every failure is logged and leaves the request alone.</p><h2>Approach</h2><p><strong>The conversation is identified by its first user message.</strong> <code>conversation_key()</code> takes the text of the
first <code>role: user</code> message and returns <code>sha1(text)[:16]</code>, or <code>None</code> when a request has no user text (a
tokenize-only call, say) so it stays out of the way. That is the same boundary the engine's prompt cache
sees, so the two agree on what "a different conversation" means.</p><p><strong><code>_auto_session_turn()</code> runs between requests, holding the service FIFO.</strong> When the incoming request's key
differs from the held one: save the held conversation to <code>auto-&lt;key&gt;.bin</code> (only if it reached
<code>auto_sessions_min_tokens</code>), then restore the incoming one if <code>auto-&lt;key&gt;.bin</code> exists. Same key → nothing
but a token count update. A save that fails does not skip the restore only in one case — a held conversation
with nothing durable, i.e. a cancelled request's provisional state, which the engine refuses as <code>invalid</code>
and which has no file worth protecting.</p><p><strong>A failed step never fails the request.</strong> The whole body is wrapped: <code>print(f"[strata] auto sessions: {e}")</code>
and the request runs as it always did. In particular, if a save failed, the held conversation is still in
the engine and still parks in RAM — restoring over it would replace a full state with a weaker one.</p><p><strong>A normal stop saves what the engine holds</strong> (<code>auto_session_shutdown_save()</code>), but only when the service is
idle: a running request's session is live and saving it would capture a partial state.</p><p><strong>Pruning touches only its own files.</strong> <code>_auto_session_prune()</code> keeps the newest <code>auto_sessions_keep</code> files
and at most <code>auto_sessions_max_gb</code> of them, and deletes only names matching <code>auto-*.bin</code> — a file saved by
hand through <code>/slots/0</code> is never removed.</p><p>Defaults, all overridable from the config: <code>auto_sessions_min_tokens</code> 16000, <code>auto_sessions_keep</code> 12,
<code>auto_sessions_max_gb</code> 100.</p><h2>Validation</h2><p><strong>Passed:</strong></p><p><em>Environment:</em> macOS 26.5.2, M5 Max 128 GB, engine <code>0.1.44.1-metal</code>, IQ3_S 79 GB, 512K context,
<code>--conversation-cache-mib 24576 --conversation-cache-slots 3</code>, <code>slot_save_path</code> set,
<code>auto_sessions_min_tokens</code> 20000.</p><ul><li>A 26,958-token conversation, then a request from a different conversation and back:
<code>auto sessions: saved 27,174 tokens (0.65 GB) to auto-&lt;key&gt;.bin in 223 ms</code>,
<code>auto sessions: restored 27,174 tokens from auto-&lt;key&gt;.bin in 146 ms</code>, answer identical.</li></ul><ul><li>Service stop with a conversation held: saved on shutdown, restored on the next start
(<code>cached=26,987</code> of 26,992 prompt tokens).</li></ul><ul><li><code>python3 -m py_compile serve/server.py</code> clean.</li></ul><ul><li>Restore does not fire for conversations below <code>auto_sessions_min_tokens</code>: logged as skipped, request
unaffected.</li></ul><p><strong>Failed:</strong> none observed.</p><p><strong>Not run:</strong></p><ul><li>A CUDA/HIP/SYCL engine. The change is <code>serve/</code> only and does not depend on the backend, but this machine
has no such hardware.</li></ul><ul><li><code>parallel</code> / <code>--batch</code> setups: <code>/slots/0</code> and this path are refused there by the pre-existing check, since
a session file could interleave with a request being admitted.</li></ul><h2>Limits</h2><ul><li><strong>It is a heuristic.</strong> A client that rewrites the first user message (a system-prompt edit prepended to
the same question, a re-rendered template) starts what looks like a new conversation and gets a new file;
one that keeps the text but changes everything after it reuses the old one. That matches what the prompt
cache does, but it is a guess.</li></ul><ul><li><strong>Disk, not RAM.</strong> The engine's <code>--conversation-cache-mib</code> pool is still the fast path; this is what covers
a restart and a conversation the pool dropped. Both are worth having.</li></ul><ul><li><strong>Depends on the engine's session file.</strong> On a Metal engine before the companion engine fix, <code>SAVE</code> fails
with <code>invalid checkpoint running-state payload</code> — the server logs it and the request proceeds. On CUDA the
path already works.</li></ul><sc-clipboard block-data="[{&quot;blockId&quot;:&quot;rcrELWC0VeOIm38Dit4jAN&quot;,&quot;blockSubtree&quot;:{&quot;block&quot;:{&quot;rcrELWC0VeOIm38Dit4jAN&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;rcrELWC0VeOIm38Dit4jAN&quot;,&quot;parentId&quot;:&quot;&quot;,&quot;type&quot;:&quot;header2&quot;,&quot;props&quot;:{&quot;title&quot;:[[&quot;Problem and result&quot;,null]]},&quot;children&quot;:[],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}}}}},{&quot;blockId&quot;:&quot;2NAHTbjyACedNb1Zo3Wh7d&quot;,&quot;blockSubtree&quot;:{&quot;block&quot;:{&quot;2NAHTbjyACedNb1Zo3Wh7d&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;2NAHTbjyACedNb1Zo3Wh7d&quot;,&quot;parentId&quot;:&quot;&quot;,&quot;type&quot;:&quot;text&quot;,&quot;props&quot;:{&quot;title&quot;:[[&quot;The engine has held a saved conversation across restarts for a while: &quot;,null],[&quot;--slot-save-path&quot;,[[&quot;c&quot;]]],[&quot; plus\n&quot;,null],[&quot;POST /slots/0?action=save|restore&quot;,[[&quot;c&quot;]]],[&quot; write and read a session file, and &quot;,null],[&quot;SAVE <path>&quot;,[[&quot;c&quot;]]],[&quot; / &quot;,null],[&quot;RESTORE <path>&quot;,[[&quot;c&quot;]]],[&quot; on\nthe engine's control stream does the work. What was missing is anything that &quot;,null],[&quot;calls&quot;,[[&quot;i&quot;]]],[&quot; it.&quot;,null]]},&quot;children&quot;:[],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}}}}},{&quot;blockId&quot;:&quot;5gcnj8ZOi41V2idSUcCs1S&quot;,&quot;blockSubtree&quot;:{&quot;block&quot;:{&quot;5gcnj8ZOi41V2idSUcCs1S&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;5gcnj8ZOi41V2idSUcCs1S&quot;,&quot;parentId&quot;:&quot;&quot;,&quot;type&quot;:&quot;text&quot;,&quot;props&quot;:{&quot;title&quot;:[[&quot;So a single-slot service had exactly one way to survive a conversation switch: the in-RAM cache, bounded by\n&quot;,null],[&quot;--conversation-cache-mib&quot;,[[&quot;c&quot;]]],[&quot;. That bound is easy to hit — at 512K a 27k-token conversation parks 651 MB, so a\n24 GiB ceiling holds roughly thirty of them and then starts evicting. And a restart, or a conversation that\nwas evicted, pays a full re-read of a prompt that can be minutes of prefill.&quot;,null]]},&quot;children&quot;:[],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}}}}},{&quot;blockId&quot;:&quot;WDmTos5sZhTSmNaor990fj&quot;,&quot;blockSubtree&quot;:{&quot;block&quot;:{&quot;WDmTos5sZhTSmNaor990fj&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;WDmTos5sZhTSmNaor990fj&quot;,&quot;parentId&quot;:&quot;&quot;,&quot;type&quot;:&quot;text&quot;,&quot;props&quot;:{&quot;title&quot;:[[&quot;After, with &quot;,null],[&quot;\&quot;auto_sessions\&quot;: true&quot;,[[&quot;c&quot;]]],[&quot;:&quot;,null]]},&quot;children&quot;:[],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}}}}},{&quot;blockId&quot;:&quot;I19oJtibYMLbbh3pcfAjVH&quot;,&quot;blockSubtree&quot;:{&quot;block&quot;:{&quot;I19oJtibYMLbbh3pcfAjVH&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;I19oJtibYMLbbh3pcfAjVH&quot;,&quot;parentId&quot;:&quot;&quot;,&quot;type&quot;:&quot;simple_table&quot;,&quot;props&quot;:{&quot;tableColumnOrder&quot;:[&quot;Mtkt96b2&quot;,&quot;Kn8kWurV&quot;,&quot;aJt1yuCl&quot;],&quot;tableColumnWidthMap&quot;:{&quot;Mtkt96b2&quot;:120,&quot;Kn8kWurV&quot;:120,&quot;aJt1yuCl&quot;:120},&quot;tableRowHeader&quot;:true},&quot;children&quot;:[&quot;yOWkqSmlOKEb50hoTnMp0p&quot;,&quot;AeZAqiusskduslGyNeTset&quot;,&quot;78tTSMxKLTT17Y5kBqxpUj&quot;,&quot;OYofUa6yNtyGeOqp194vKZ&quot;],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}},&quot;yOWkqSmlOKEb50hoTnMp0p&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;yOWkqSmlOKEb50hoTnMp0p&quot;,&quot;parentId&quot;:&quot;I19oJtibYMLbbh3pcfAjVH&quot;,&quot;type&quot;:&quot;simple_table_row&quot;,&quot;props&quot;:{},&quot;children&quot;:[&quot;B70Qg2zjOVCwgyvjgCuGai&quot;,&quot;rCFtb4rzQFDyFiEYb7bFT6&quot;,&quot;b1tW5yZOfuLypzKEFzGE7r&quot;],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}},&quot;B70Qg2zjOVCwgyvjgCuGai&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;B70Qg2zjOVCwgyvjgCuGai&quot;,&quot;parentId&quot;:&quot;yOWkqSmlOKEb50hoTnMp0p&quot;,&quot;type&quot;:&quot;simple_table_cell&quot;,&quot;props&quot;:{&quot;columnKey&quot;:&quot;Mtkt96b2&quot;},&quot;children&quot;:[&quot;PEUeNVYbP9CXtp2FphygsJ&quot;],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}},&quot;PEUeNVYbP9CXtp2FphygsJ&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;PEUeNVYbP9CXtp2FphygsJ&quot;,&quot;parentId&quot;:&quot;B70Qg2zjOVCwgyvjgCuGai&quot;,&quot;type&quot;:&quot;text&quot;,&quot;props&quot;:{&quot;title&quot;:[]},&quot;children&quot;:[],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}},&quot;rCFtb4rzQFDyFiEYb7bFT6&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;rCFtb4rzQFDyFiEYb7bFT6&quot;,&quot;parentId&quot;:&quot;yOWkqSmlOKEb50hoTnMp0p&quot;,&quot;type&quot;:&quot;simple_table_cell&quot;,&quot;props&quot;:{&quot;columnKey&quot;:&quot;Kn8kWurV&quot;},&quot;children&quot;:[&quot;MOHs5rkvETGkCQeBNjOKrJ&quot;],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}},&quot;MOHs5rkvETGkCQeBNjOKrJ&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&quot;:{&quot;id&quot;:&quot;MOHs5rkvETGkCQeBNjOKrJ&quot;,&quot;parentId&quot;:&quot;rCFtb4rzQFDyFiEYb7bFT6&quot;,&quot;type&quot;:&quot;text&quot;,&quot;props&quot;:{&quot;title&quot;:[[&quot;before&quot;,null]]},&quot;children&quot;:[],&quot;parentTable&quot;:&quot;block&quot;,&quot;enabled&quot;:true}},&quot;b1tW5yZOfuLypzKEFzGE7r&quot;:{&quot;role&quot;:&quot;editor&quot;,&quot;value&qu

関連リンク

インストール・モデル・リリースへのサイト内リンク。