Repository navigation
Fix the MLX server dying at startup on CI, and show why it did - #39
Merged
Merged
Conversation
The servers wrote into pipes nobody read, and the tests polled a dead server until their 15-minute timeout without its output. Write the output to a file, fail as soon as the server exits, and on a timeout abort it with faulthandler on, so the failure shows the traceback or the stuck stacks.
The evaluation after the layers converted both, about 2.4 GB of memory in one command buffer, five times a layer's. On CI runners short of memory, Metal aborted it and the server died at startup with kIOGPUCommandBufferCallbackErrorTimeout; evaluated apart, each still did. Cast both on the CPU, which gives the same values and has no Metal time limit, and quantize lm_head in blocks of rows, a third of a layer each. The loaded parameters are bit-identical for every precision.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mlx-integration-testfailed on main after #38 (run 42134257749). The test reported onlyTimed out waiting for the MCP server to become ready.after 15 minutes. The server had in fact died within a minute of starting, while loading the model:This PR makes such failures visible at once, and keeps the evaluation that failed off long GPU command buffers.
1. The test hid the crash
The integration tests started the server with
stdout=PIPE, stderr=PIPEand never read either pipe. They also never checked whether the server was alive, so they polled a dead port until the timeout.The tests now:
PYTHONFAULTHANDLER=1, so the failure shows where each thread was stuck.The traceback above was captured this way.
2. Metal aborted the load's largest command buffer
The CI runners are virtual 3-core M1s with 7.5 GB of memory, and they compress memory heavily while the model loads. The CLI also sets
MLX_MAX_MB_PER_BUFFER=1000000, so that decoding does not end a command buffer after every matrix. During loading, this lets one evaluation become one command buffer.The evaluation after the layer loop converted both the embedding and lm_head, each 107,520 × 2,560 (550 MB in BF16). That is about 2.4 GB of memory in one command buffer, five times a layer's.
The crash is rare: it happened in 2 of about 140 natural server starts on the runners. Load experiments on the runners isolated the cause:
Ruled out: the GPU waiting for disk reads. MLX loads safetensors on the CPU, and a 43 s GPU wait on CPU work passed on the runners.
Two other designs made things worse, so they are not used here:
async_evalper parameter.The fix
load_modelcasts both on the CPU. BF16 → FP16 gives bit-identical values there (checked on all 243 tensors), takes 0.08 s, and has no Metal time limit.Plamo3Translatorevaluates these casts on the CPU alone, after the layers.Verification
Parameters: all loaded parameters are bit-identical to main for 4bit, mixed, 8bit and bf16 (437 / 437 / 437 / 243 tensors, compared by MD5).
Translations: identical for 4bit, mixed and bf16.
Memory: peak is unchanged (2.57 GB for 4bit, 6.0 GB for bf16). Active memory after loading equals the parameter bytes.
Load time: unchanged within noise (4bit: main 4.6–5.4 s, this PR 4.6–5.0 s on M1 Max).
Tests: mock tests pass (121), and so do the real-model MLX tests (38).
On the CI runners:
main did not fail in those runs either, so they show no regression rather than a measured improvement. The failing evaluation is gone by construction: no load command buffer is larger than a layer's.
Not in this PR
mlx-integration-testruns only on pushes to main, so this failure surfaced after merging. Running it on PRs that touchservers/mlx/would catch such problems earlier.config.json(is_plamo3_checkpoint), which logs an unauthenticated-request warning.