Studio autoload: retry the folder's next quant when a load itself fails
Fifth round of review follow-ups. A resolved quant that passed validation could still fail /api/inference/load (corrupt file, llama.cpp startup error); the cascade then abandoned the whole row because only validation blocks recorded a skip key. The fallback now marks the failed quant skipped and resolves the folder's next complete quant before moving on, bounded by the existing attempt cap. Single-candidate rows resolve to null once skipped, so the retry loop terminates. Contract test and simulation added for the failure-then-retry path.
This commit is contained in:
parent
a928900a2e
commit
08cb60c6e1
2 changed files with 57 additions and 10 deletions
|
|
@ -1568,8 +1568,12 @@ async function resolveLocalRowCandidate(
|
|||
return null;
|
||||
}
|
||||
}
|
||||
const candidate = localRowToCandidate(row, isGguf ? rememberedVariant : null);
|
||||
// Single-candidate rows resolve to null once their candidate is skipped,
|
||||
// so the retry loop in the fallback terminates instead of re-attempting.
|
||||
if (isSkippedCandidate?.(candidate)) return null;
|
||||
return {
|
||||
candidate: localRowToCandidate(row, isGguf ? rememberedVariant : null),
|
||||
candidate,
|
||||
sizeBytes: sizeOrUnknownBytes(row.size_bytes),
|
||||
};
|
||||
}
|
||||
|
|
@ -2262,20 +2266,42 @@ export async function autoLoadOnDeviceModel(): Promise<{
|
|||
continue;
|
||||
}
|
||||
const row = candidate.row;
|
||||
const localCandidate = candidate.candidate;
|
||||
let localCandidate: AutoLoadCandidate | null = candidate.candidate;
|
||||
if (isSeen(localCandidate.kind, row.load_id, row.id, row.path)) {
|
||||
continue;
|
||||
}
|
||||
markSeen(localCandidate.kind, row.load_id, row.id, row.path);
|
||||
if (isSkippedAutoLoadCandidate(localCandidate)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (await loadAutoLoadCandidate(localCandidate)) {
|
||||
return { loaded: true, blockedByTrustRemoteCode: false };
|
||||
// Try the row's quants smallest-first: a failed LOAD (not just a
|
||||
// blocked validation) marks that quant skipped and the folder's next
|
||||
// complete quant is resolved and tried, so one corrupt file cannot
|
||||
// abandon a folder that still holds a loadable quant. The attempt cap
|
||||
// still bounds total /load calls; single-candidate rows resolve to
|
||||
// null once skipped, terminating the loop.
|
||||
while (localCandidate && loadAttempts < MAX_AUTO_LOAD_ATTEMPTS) {
|
||||
if (!isSkippedAutoLoadCandidate(localCandidate)) {
|
||||
try {
|
||||
if (await loadAutoLoadCandidate(localCandidate)) {
|
||||
return { loaded: true, blockedByTrustRemoteCode: false };
|
||||
}
|
||||
} catch {
|
||||
hadNonTrustFailure = true;
|
||||
skippedAutoLoadCandidates.add(
|
||||
autoLoadCandidateKey(
|
||||
localCandidate.kind,
|
||||
localCandidate.id,
|
||||
localCandidate.ggufVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
localCandidate =
|
||||
(await resolveLocalRowCandidate(row, null, isSkippedAutoLoadCandidate))
|
||||
?.candidate ?? null;
|
||||
} catch {
|
||||
hadNonTrustFailure = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
hadNonTrustFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -708,3 +708,24 @@ def test_local_fallback_orders_by_resolved_quant_size():
|
|||
"const ggufGroup: FallbackCandidate[]"
|
||||
)
|
||||
assert "sizeBytes: resolved.sizeBytes" in auto_load
|
||||
|
||||
|
||||
def test_cascade_retries_next_quant_after_load_failure():
|
||||
"""A failed /api/inference/load (not just a blocked validation) must mark
|
||||
that quant skipped and try the folder's next complete quant before the
|
||||
row is abandoned; single-candidate rows resolve to null once skipped so
|
||||
the retry loop terminates, and the attempt cap bounds total loads."""
|
||||
src = _read("features/chat/api/chat-adapter.ts")
|
||||
auto_load = src.split("async function autoLoadOnDeviceModel", 1)[1]
|
||||
assert (
|
||||
"while (localCandidate && loadAttempts < MAX_AUTO_LOAD_ATTEMPTS)" in auto_load
|
||||
)
|
||||
# The cascade catch records the failed quant, unlike the old generic flag.
|
||||
local_loop = auto_load.split(
|
||||
"while (localCandidate && loadAttempts < MAX_AUTO_LOAD_ATTEMPTS)", 1
|
||||
)[1].split("\n }", 1)[0]
|
||||
assert "skippedAutoLoadCandidates.add(" in local_loop
|
||||
# Termination guard: a skipped single candidate resolves to null.
|
||||
resolve_fn = src.split("async function resolveLocalRowCandidate", 1)[1]
|
||||
resolve_fn = resolve_fn.split("\nfunction ", 1)[0]
|
||||
assert "if (isSkippedCandidate?.(candidate)) return null;" in resolve_fn
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue