Skip to content

Commit ebf0b08

Browse files
peopleworksclaude
andcommitted
Keep-alive: client pre-warms the model to hide idle cold-start
Adds GET /api/warmup that lazy-loads the model (and resets the idle clock) without scoring — fast if already loaded. The Blazor panel fires it fire-and-forget when it mounts (right after the user's first analysis), so the model loads (~1.5s) during the seconds before they click Measure. Reconciles the idle-unload (frees ~1.2GB when the site is quiet) with a warm experience while the site is in active use — self-regulating, no always-on pinger. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4b5179f commit ebf0b08

5 files changed

Lines changed: 31 additions & 0 deletions

File tree

src/SignsOfAI.Perplexity.Api/Engine/IPerplexityEngine.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ public interface IPerplexityEngine
2525

2626
/// <summary>Runs one forward pass and returns the perplexity of <paramref name="text"/>.</summary>
2727
Task<PerplexityRaw> ScoreAsync(string text, CancellationToken ct = default);
28+
29+
/// <summary>Ensures the model is loaded into RAM (and resets the idle clock) without scoring —
30+
/// so a subsequent request is warm. Cheap if already loaded.</summary>
31+
Task WarmupAsync(CancellationToken ct = default);
2832
}

src/SignsOfAI.Perplexity.Api/Engine/OnnxPerplexityEngine.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ public async Task<PerplexityRaw> ScoreAsync(string text, CancellationToken ct =
7272
finally { Release(); }
7373
}
7474

75+
/// <summary>Loads the model (if needed) and refreshes the idle clock, without running inference.</summary>
76+
public async Task WarmupAsync(CancellationToken ct = default)
77+
{
78+
if (!_filesReady) return;
79+
_ = await AcquireAsync(ct);
80+
Release();
81+
}
82+
7583
// ── Load / unload lifecycle ──────────────────────────────────────────────
7684
private async Task<(InferenceSession, Tokenizer)> AcquireAsync(CancellationToken ct)
7785
{

src/SignsOfAI.Perplexity.Api/Program.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
app.MapGet("/healthz", (IPerplexityEngine engine) =>
4545
engine.IsReady ? Results.Ok("ready") : Results.StatusCode(503));
4646

47+
// Cheap pre-warm: clients call this when the user is about to measure so the model is already
48+
// in RAM (hides the ~1.5s cold reload after an idle-unload). Returns fast if already loaded.
49+
app.MapGet("/api/warmup", async (IPerplexityEngine engine, CancellationToken ct) =>
50+
{
51+
if (!engine.IsReady) return Results.Json(new { modelLoaded = false, ready = false });
52+
var sw = System.Diagnostics.Stopwatch.StartNew();
53+
await engine.WarmupAsync(ct);
54+
return Results.Json(new { modelLoaded = engine.IsLoaded, ready = true, elapsedMs = sw.ElapsedMilliseconds });
55+
});
56+
4757
app.MapPost("/api/perplexity", async (
4858
PerplexityRequest req, IPerplexityEngine engine, PerplexityScorer scorer, CancellationToken ct) =>
4959
{

src/SignsOfAI.Web/Components/PerplexityPanel.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
{
8787
_settings = await Perplexity.LoadSettingsAsync(Storage);
8888
_endpointEdit = _settings.Endpoint;
89+
// Pre-warm the server while the user reads their results, so "Measure" is warm.
90+
if (_settings.IsConfigured) _ = Perplexity.WarmupAsync(_settings.Endpoint);
8991
}
9092

9193
private async Task SaveEndpoint()

src/SignsOfAI.Web/Services/PerplexityClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ public async Task<PerplexitySettings> LoadSettingsAsync(BrowserStorage storage)
6060
public async Task SaveSettingsAsync(BrowserStorage storage, PerplexitySettings settings) =>
6161
await storage.SetAsync(StorageKey, JsonSerializer.Serialize(settings, PerplexityJsonContext.Default.PerplexitySettings));
6262

63+
/// <summary>Fire-and-forget pre-warm so the server model is loaded before the user clicks Measure.</summary>
64+
public async Task WarmupAsync(string endpoint, CancellationToken ct = default)
65+
{
66+
try { using var _ = await http.GetAsync(endpoint.TrimEnd('/') + "/api/warmup", ct); }
67+
catch { /* best-effort; a cold measure just costs ~1.5s */ }
68+
}
69+
6370
/// <summary>Measures perplexity for <paramref name="text"/>. Throws with a friendly message on failure.</summary>
6471
public async Task<PerplexityResult> MeasureAsync(string endpoint, string text, string lang, CancellationToken ct = default)
6572
{

0 commit comments

Comments
 (0)