Replies: 3 comments 1 reply
|
核验通过(对照 main/master HEAD 机制逐条确认:
修复优先级(同意你的四项,补充一个先决动作):
一个补充观察: 需要的话我可以写一个微基准回归(1k/30k/120k/200k 的 append+read 表,对比游标 vs 快照),挂到 |
|
I verified the three relevant source paths at https://sandbaseai.github.io/deepseek-harness-handbook/slow-ttft.html The reusable diagnostic is a four-timestamp split:
This separates Host outbound ( Two source nuances worth preserving in any fix/benchmark:
The guide keeps your |
|
The three mechanisms are solidly verified by zoahdev + denial123789, so I won't re-litigate them. One angle on fix direction 2 (batching) that the thread hasn't covered — on-disk durability/recoverability, from the session-log corruption family (13 reports, 11 mechanisms tracked across #1333/#1452/#1497/#2167/#2839/#3198/#3450 et al.). 1. Torn-frame semantics make batch size a loss-granularity decision. The JSONL backend writes one zstd frame per append, and the loader already drops an incomplete FINAL frame (committed-prefix semantics — session-persistence-jsonl/src/index.ts:245, torn-frame recovery at :347-383). So a crash mid-write loses the whole in-flight append, never a partial record. Today that costs ≤1 chunk. With "one append per step" it costs the step's entire streamed output — for a long reasoning turn that's minutes of tokens gone, even though per-append overhead drops. A time-bounded flush (~100ms) caps the loss at ~100ms of tokens while still cutting the append count from ~7k/turn to ~tens. The file-size win survives; the loss window doesn't grow with step length. 2. Batching widens the "resume seed ≠ persisted tail" window — the family's systemic risk. The corruption family's common thread is divergence between the in-memory log and the committed file at resume: #2839 (stale prepared-source cache + write-behind revision lag), #2167 (same-process stale-view re-append), #3198 (seed boundary captured before end-seed append — 3. 4. Replay ordering (#3450) is orthogonal — no new risk. Batches are consecutive seqs; append order = seq order, so batching cannot reorder within or across frames. The turn-tail violation comes from compaction's single replace + replay emitting shadowed copies in seq order across a turn boundary — granularity doesn't touch it. Net: batch direction is sound with three constraints — bounded flush (time/byte-cap), synchronous end-seed/turn-end, and pairing with the append-contract hardening rather than landing alone. Happy to write the micro-benchmark (append+read across 1k/30k/120k/200k with batched vs per-chunk frames) if that helps pick the flush window. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
TL;DR
On a self-hosted
dsh web(v0.1.0-rc.7, systemd unit, one Node process), time-to-first-token as seen in the UI grows from ~10s on fresh sessions to a median 49.7s / p90 142s on mature, multi-session hosts, while the model gateway answers the same requests with a 2.1s median TTFT. I instrumented both ends (laptop session logs + the gateway router's sqlite request log) and micro-benchmarked the installeddsh-sessionclass. Three host-side mechanisms explain the gap; all three have plausible small fixes.Evidence chain
1. Gateway is not the bottleneck. 441 requests matched one-to-one between laptop session logs (step/start → first chunk) and the gateway router's
requestDetailsrows (same window, 2026-08-18 22:07 UTC →):Of 220 requests with laptop TTFT >50s, 195 had gateway TTFT ≤10s. Both the outbound leg (arrival p90 30s) and the inbound leg (return median 35s) lose time inside the host process boundary.
2. It scales with concurrent in-flight steps in the one host process. TTFT by number of concurrent active model steps across all sessions (main + subagents, same host):
Fresh-spawn subagents (own session, own log) reproduce it and this is not a fork-only effect. Restarting
dsh webimmediately restores ~10s TTFT (sessions resume from disk), and it degrades again over hours.3.
session.eventssnapshot rebuild is O(log) per append. POC against the real installedSessionclass (rc.7), reproducing the production read pattern (token-meter_syncreadssession.eventson everysession/event, i.e. every streamed chunk):eventsread (ms)Append is flat O(1); the getter rebuild (
Object.freeze([...this.log]),dsh-session/lib/index.js:1397-1400, invalidated per append at 1470) is O(n). One of my real sessions holds 94kreasoning-chunks+ 58kassistant/chunkevents, so a single streamed turn (~7k chunk appends) pays ~0.5s at 30k log length and ~6.7s at 200k. per concurrent session, on the shared event loop, on top of per-event observer fan-out (api-proxy broadcast, zstd flush).Ask / fix directions
_synconly needs "new events since my cursor".session.log.slice(consumed)(or an iterator) avoids the full-copy+freeze per chunk entirely. The snapshot getter can stay for external consumers.subagent_forkseeding does a synchronous full-log validate/deep-freeze of the parent's log (events.slice(0, lastEnd.seq+1)→Sessionconstructor) : a ~100k-event blocking copy on the shared loop. Chunked/deferred seeding, or freezing lazily per accessed depth, would remove the stall.Environment: dsh CLI 0.1.0-rc.7, Node v26.5.0, web profile via systemd user unit, 3-5 concurrent sessions typical, orchestrator preset with named subagents. Happy to share the POC script, the join scripts, or raw numbers if useful.
All reactions