Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,24 @@ jobs:
env:
HF_HUB_DOWNLOAD_TIMEOUT: "600"
SWIFTLM_TEST_MODEL: ${{ matrix.test_model }}
# Diagnostic: print a Swift backtrace when the server dies with a signal.
SWIFT_BACKTRACE: "enable=yes,interactive=no"
run: |
chmod +x tests/test-${{ matrix.modality }}.sh
for attempt in 1 2 3; do
echo "Attempt $attempt of 3..."
# Diagnostic: collect the SwiftLM crash report from every attempt, not only when the
# job fails, because the job retries and a crash in attempt 1 is otherwise invisible.
touch "$RUNNER_TEMP/attempt-$attempt"
if tests/test-${{ matrix.modality }}.sh .build/release/SwiftLM 15413; then exit 0; fi
# ReportCrash writes the report a moment after the server dies, so wait before looking.
sleep 10
reports=$(find "$HOME/Library/Logs/DiagnosticReports" -name '*SwiftLM*' -newer "$RUNNER_TEMP/attempt-$attempt" 2>/dev/null || true)
if [ -z "$reports" ]; then echo "no new SwiftLM crash report for attempt $attempt"; fi
for f in $reports; do
echo "===== crash report, attempt $attempt: $f"
head -c 60000 "$f"
done
if [ "$attempt" -eq 3 ]; then echo "All attempts failed"; exit 1; fi
sleep 10
done
Expand Down
14 changes: 10 additions & 4 deletions Sources/SwiftLM/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2765,8 +2765,8 @@ func handleChatCompletion(
// layers (RotatingKVCache under --ctx-size; this path must run before the
// sliding-window one, which misses on any MambaCache), and the generic path below
// refuses them: recurrent state cannot be
// trimmed, and the onPrefillDone save runs after the first decode token has been
// fed. Without this, every agent turn re-prefills the whole conversation. Instead:
// trimmed, and the onPrefillDone save runs once the producer has emitted `.info`.
// Without this, every agent turn re-prefills the whole conversation. Instead:
// resume from an exact cached prefix, prefill to the turn boundary, snapshot there
// synchronously, then generate the few tokens after it.
let hybridBoundary = (skipPromptCache || draftModelRef != nil || config.mtp || config.turboKV)
Expand Down Expand Up @@ -3302,7 +3302,6 @@ func handleChatStreaming(
let memSnap = MemoryUtils.snapshot()
print("srv slot update: id 0 | prefill done | n_tokens=\(promptTokenCount), t=\(String(format: "%.2f", prefillDur))s, \(String(format: "%.1f", prefillTokPerSec))t/s | OS_RAM=\(String(format: "%.1f", memSnap.os))GB | MEM_DEMAND=\(String(format: "%.1f", memSnap.demand))GB | GPU_MEM=\(String(format: "%.1f", memSnap.gpu))GB")
print("srv generate: id 0 | ", terminator: "")
if let onPrefillDone { await onPrefillDone() }
firstToken = false
}
if tokenEcho {
Expand Down Expand Up @@ -3413,6 +3412,11 @@ func handleChatStreaming(
print("[SwiftLM] Rejected tool call: reason=\(rejection.reason) tool=\(rejection.toolName ?? "?") detail=\(rejection.detail ?? "n/a")")

case .info(let info):
// `.info` is the producer's last yield, so the cache is no longer written once
// it arrives. The prompt is saved here, not on the first chunk: that raced the
// decode step the producer had already started on the same KVCacheSimple. An
// early exit (stop sequence, disconnect) never reaches `.info` and skips the save.
if !firstToken, let onPrefillDone { await onPrefillDone() }
// The chunk count undercounts: tokens the decoder buffers (tool-call
// bodies, partial text) emit no chunk. `info` carries the real count.
completionTokenCount = info.generationTokenCount
Expand Down Expand Up @@ -3568,7 +3572,6 @@ func handleChatNonStreaming(
let memSnap = MemoryUtils.snapshot()
print("srv slot update: id 0 | prefill done | n_tokens=\(promptTokenCount), t=\(String(format: "%.2f", prefillDur))s, \(String(format: "%.1f", prefillTokPerSec))t/s | OS_RAM=\(String(format: "%.1f", memSnap.os))GB | MEM_DEMAND=\(String(format: "%.1f", memSnap.demand))GB | GPU_MEM=\(String(format: "%.1f", memSnap.gpu))GB")
print("srv generate: id 0 | ", terminator: "")
if let onPrefillDone { await onPrefillDone() }
firstToken = false
}
if tokenEcho {
Expand All @@ -3587,6 +3590,9 @@ func handleChatNonStreaming(
// See the matching comment in `handleChatStreaming`: log only, no wire shape.
print("[SwiftLM] Rejected tool call: reason=\(rejection.reason) tool=\(rejection.toolName ?? "?") detail=\(rejection.detail ?? "n/a")")
case .info(let info):
// The producer's last yield; see the matching comment in `handleChatStreaming`.
// The stop-sequence `break` above exits before `.info`, so it skips the save.
if !firstToken, let onPrefillDone { await onPrefillDone() }
generationStopReason = info.stopReason
completionTokenCount = info.generationTokenCount // real count; see handleChatStreaming
}
Expand Down
Loading