-
Notifications
You must be signed in to change notification settings - Fork 1
2.4.2: guard benchmarks against out-of-memory; surface provider errors #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| """Single source of truth for the app version. Read by grammar_fix.py.""" | ||
|
|
||
| __version__ = "2.4.1" | ||
| __version__ = "2.4.2" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -254,6 +254,81 @@ def usable_memory_gb(hw: dict | None = None) -> float: | |
| return max(2.0, round(mem - _OS_HEADROOM_GB, 1)) | ||
|
|
||
|
|
||
| # `flm bench` sweeps 1k-32k context x 8 iterations, so it needs room for a large | ||
| # KV cache ON TOP of the weights. Reserve for the 32k worst case; without this a | ||
| # model that merely *loads* (weights < usable) still dies once the sweep grows | ||
| # the cache, surfacing as an opaque driver page-in failure. | ||
| _BENCH_CONTEXT_HEADROOM_GB = 4.0 | ||
|
|
||
|
|
||
| def available_memory_gb() -> float: | ||
| """Physical memory currently free, in GB (0.0 when it can't be read).""" | ||
| if os.name != "nt": | ||
| return 0.0 | ||
| import ctypes | ||
|
|
||
| class MEMORYSTATUSEX(ctypes.Structure): | ||
| _fields_ = [ | ||
| ("dwLength", ctypes.c_ulong), | ||
| ("dwMemoryLoad", ctypes.c_ulong), | ||
| ("ullTotalPhys", ctypes.c_ulonglong), | ||
| ("ullAvailPhys", ctypes.c_ulonglong), | ||
| ("ullTotalPageFile", ctypes.c_ulonglong), | ||
| ("ullAvailPageFile", ctypes.c_ulonglong), | ||
| ("ullTotalVirtual", ctypes.c_ulonglong), | ||
| ("ullAvailVirtual", ctypes.c_ulonglong), | ||
| ("ullAvailExtendedVirtual", ctypes.c_ulonglong), | ||
| ] | ||
|
|
||
| stat = MEMORYSTATUSEX() | ||
| stat.dwLength = ctypes.sizeof(stat) | ||
| if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)): | ||
| return round(stat.ullAvailPhys / 2**30, 1) | ||
| return 0.0 | ||
|
|
||
|
|
||
| def benchmark_fit( | ||
| model: str, | ||
| footprint_gb: float | None, | ||
| hw: dict | None = None, | ||
| *, | ||
| available_gb: float | None = None, | ||
| ) -> dict: | ||
| """Can this model be benchmarked on this machine? -> {ok, error, needed_gb, usable_gb}. | ||
|
|
||
| Benchmarking is stricter than merely running a model: the context sweep adds | ||
| a large KV cache to the resident weights. Compared against | ||
| :func:`usable_memory_gb` (stable, matches what the dashboard shows) rather | ||
| than instantaneous free memory, because the benchmark stops the serve server | ||
| first and thereby reclaims whatever the active model was holding. | ||
|
|
||
| An unknown footprint never blocks the run — we only refuse on evidence.""" | ||
| hw = hw or detect_hardware() | ||
| usable = usable_memory_gb(hw) | ||
|
Comment on lines
+306
to
+307
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On a Ryzen AI machine that also has an NVIDIA GPU, Useful? React with 👍 / 👎. |
||
| try: | ||
| footprint = float(footprint_gb) if footprint_gb is not None else None | ||
| except (TypeError, ValueError): | ||
| footprint = None | ||
| if not footprint or footprint <= 0: | ||
| return {"ok": True, "error": "", "needed_gb": None, "usable_gb": usable} | ||
| needed = round(footprint + _BENCH_CONTEXT_HEADROOM_GB, 1) | ||
| if needed <= usable: | ||
| return {"ok": True, "error": "", "needed_gb": needed, "usable_gb": usable} | ||
| free = available_memory_gb() if available_gb is None else float(available_gb) | ||
| detail = f"{free:g} GB free right now" if free else "free memory unknown" | ||
| return { | ||
| "ok": False, | ||
| "needed_gb": needed, | ||
| "usable_gb": usable, | ||
| "error": ( | ||
| f"'{model}' is too large to benchmark on this machine: it needs about " | ||
| f"{needed:g} GB (~{footprint:g} GB of weights plus room for the 32k-context " | ||
| f"sweep) but only ~{usable:g} GB is usable ({detail}). Benchmark a smaller " | ||
| f"model, or run this one on a machine with more memory." | ||
| ), | ||
| } | ||
|
|
||
|
|
||
| def _normalize_candidate(candidate: object) -> dict: | ||
| """Accept either a legacy ``(name, params_b)`` tuple or a metadata dict.""" | ||
| if isinstance(candidate, dict): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When FastFlowLM returns the HTTP-200
{"error": ...}response for a request withstream: true, the normal dashboard path never reaches this new branch:sendChat()useschat_send_stream, while_default_llm_stream()feeds the response to_parse_sse_delta(), which ignores both non-data:JSON bodies and SSE objects withoutchoices.stream_send()consequently emits a successful terminal event with no text, so the user still sees(no reply)instead of the provider's load failure. Apply the same error extraction to the streaming response path.Useful? React with 👍 / 👎.