Repository navigation
lfm3: add native Metal vision support above the text decoder - #1502
BlivionIaG wants to merge 2 commits into
Conversation
|
Thanks @BlivionIaG for a careful port and an honest receipt. The same-engine checks are thorough (231 served, with cancellation and recovery), and the rotary fix comes with a test that fails on the old formula. The merge with main is clean. Here is our answer on the gates. Fidelity:
Performance, on the same Mac and checkpoint against mlx-lm (mlx-vlm for VL), at each width:
Before this leaves draft:
Once the gates hold, we'll run the full Metal contract and the served checks on our M5 Ultra. |
|
@BlivionIaG, one small pixel-parity note on transformers' The resize ( value.* = (@as(f32, @floatFromInt(i)) - 127.5) / 127.5;With that, the processor's output should be reproduced bit for bit.
|
|
The Linux CI failure at Adding |
's image tests from a build file of ours C11 put test-lfm3-image-host and a sys import into zig/build/lfm3_host.zig, a file ashhart#1502 edits too. That file is back to its version below C11, so ashhart#1502's own edit of it applies. zig/build/lfm3_xpu_host.zig declares the step instead, and only while no lfm3_host.zig declares it; build/sys.zig's walk gives the test root its sys import, so none is named here.
…s within 1.7e-3 of float64 Every activation of the tower and the projector stays fp32 (weights bf16 as stored, sums fp32 as before); only the image rows go to the decoder in bf16, as ashhart#1502's rows buffer holds them. Each kernel keeps a rounding flag for transformers' bf16 points (vision.Round, Tower.round; 0 by default): with every bit set the forward is C11's bit for bit. The ablation on ashhart#1502's 14 x 20 fixture against vision_fp64_reference.py in plain float64: no one rounding point carries the gap (any one alone in fp32 leaves the rows at 2.7e-2 to 7.0e-2); taken together they do: the linear outputs, the conv's inner points and the attention path to 7.5e-3, the projector's three points to 3.3e-3, the pixels to 1.65e-3 (layer 13 at 4.3e-6). - fixture: image rows rel 1.65e-3, cos 0.999999 (C11: 4.21e-2, 0.99912); transposed grid 0.82; - the 3000 x 4000 form photo at 1,024 tokens: rel 1.66e-3, cos 0.999999; transposed 1.09. tf-xpu-lfm3_vit-test now runs the kernels with fp32 in and out, within 1e-5 of float64 (at most 1.6e-6), and its controls still fail, GELU's tanh form among them. tf-xpu-lfm3_tower-test dumps fp32 and the first layers' stages by ashhart#1502's trace names; LFM3_VIT_ROUND sets the rounding points.
…ed row for ashhart#1502's compare_logits.py tf-xpu-lfm3_vl_logits-test runs ashhart#1502's real-image fixture (90 tokens, 70 image rows at 4, the suffix "Describe this image." -> "The image contains a cat.") through the tower and the decoder in windows of 16 rows, and writes each row's logits as bf16, the projected rows and the patches. On a stand-in for ashhart#1502's cat (placecats.com/300/200 serves another JPEG now: 14 x 20 patches, 70 rows), against ashhart#1502's fp64_reference.py, compare_logits.py and vision_fp64_reference.py, unchanged: - the XPU's own rows into float64 (decoder alone): rel 1.72e-2 over 90 rows (ashhart#1502: 1.30e-2), 3 top-1 differences, 1 disjoint top set, all at image positions; the suffix: rel 1.17e-2, none; - float64's own vision (whole pipeline): rel 3.74e-2, 3 disjoint top sets over 90 rows (ashhart#1502: 7), all at image positions; the suffix: rel 1.30e-2, no top-1 difference (ashhart#1502: 2 disjoint).
|
@BlivionIaG, the macOS dev-tools job on This is from the CI compiler log, without a local build. The other failed jobs still report |
|
Thanks @BlivionIaG. The new description is what we asked for, and stepped prefill is a real gain: a short request
On fidelity, we'll judge the two rows only the native route misses (VL 4-bit row 3, and VL 6-bit at 32K) against |
|
@BlivionIaG, a small request for a like-for-like check. I'm porting LFM3-VL's vision path to Intel's XPU backend, taking Could you attach the canonical PNG from No change to #1502 is needed for this. |
|
Thanks @ashhart. The three options are off in every build in this PR (pushing now), and the speed numbers will be labelled as the opt-in build. For your call on the two rows, here is what the receipts say about the tiled route against the two references on #1491 (independent FP32 forward, official MLX BF16). No pass rule is applied; every difference is listed in the receipts. RMS to FP32, native route over official BF16 (below 1 is closer to FP32):
The two rows only the native route misses:
A tie at the BF16 maximum is decided by index, not by the arithmetic. The receipts also list the rows both routes miss, for example the VL 4-bit 32K prompt row where native and official BF16 tie and both miss FP32 by 0.085. |
|
@jschmied here is the canonical PNG, committed at |
…hes file An optional fifth argument, fp32 [patches][1536] for the picture's grid, replaces the decoded pixels: a receipt's own patches go through the tower and the decoder unchanged, e.g. ashhart#1502's real-image fixture made with the pixel normalization before 50b4e0d (patches sha f1c55a0dd28b as its .npy).
|
Thanks @ashhart. All three are done on this head.
With chunks capped at 512 rows the server's tokens are unchanged (
The flag trades prompt throughput for inter-token latency (aggregate prefill 11.6k and 16.4k tok/s at 2k and 8k, against 22.8k and 23.4k for mlx-vlm). Without it, stepped prefill leaves p95 ITL at 45.8 ms at 8k, which is the cost of finishing prompts first. |
|
Thanks @BlivionIaG, the PNG was exactly what was needed. Here is the rerun of The receipt's
There is no top-1 difference at a text row in either run. They are all at image positions. With the current, corrected patches ( So |
zig/src/families/lfm3/image.zig and its test root zig/src/lfm3_image_tests.zig byte-identical to ashhart#1502 at f066644, so they drop out when ashhart#1502 lands.
…ild file of ours zig/build/lfm3_xpu_host.zig declares the step only while no other build file has, so ashhart#1502's own lfm3_host.zig step takes over when it lands; build/sys.zig's walk gives the test root its sys import.
…ainst ashhart#1502's float64 reference xpu_vision.zig loads the 138 bf16 tensors by ashhart#1502's names and shapes and runs one picture's merge-order patches to its gh * gw / 4 image rows: patchifier and embedding norm, the 14 layers (gated depthwise conv or GQA attention with QK norm and axial RoPE, each with the SwiGLU FFN), then the projector (LayerNorm a patch, 2 x 2 blocks as 4096-wide rows, linear_1, GELU, linear_2). zig/src/lfm3_image.zig roots ashhart#1502's image.zig where its relative imports reach. tf-xpu-lfm3_tower-test writes the patches, every layer's rows and the image rows, and the same patches with the grid passed transposed. tools/lfm3/vision_check.py runs ashhart#1502's numpy reference (vision_fp64_reference.py, unchanged) with its bf16 points and in plain float64: - 14 x 20 synthetic picture: image rows rel 4.21e-2, cos 0.99912 against float64 (the reference's bf16 points: rel 6.55e-2); the transposed grid: rel 0.82, cos 0.67; - a 3000 x 4000 photo of a form at 1,024 image tokens (72 x 54 patches): rel 2.84e-2, cos 0.99960 (bf16 points: rel 3.13e-2); transposed: rel 1.09, cos 0.41. The tower takes 164 ms on the synthetic picture and 2.97 s on the photo on Iris Xe.
…ed in place of the image tokens xpu-native reads lfm3_vl checkpoints with the LFM3 family (vl_model_type) and offers image input as request unless --vision-image-tokens names 64 to 4096; LFM3-1.7B still refuses --vision. The engine loads the tower with scratch for that budget. Before a stream's prompt windows, the adapter hands its pictures to the engine (pictures, when the engine has it): the tower writes their rows, and each prompt window copies the rows over the embeddings of the image tokens it holds. Decode, copy drafts, streams and the prompt cache are unchanged; a kept state after a picture is keyed by the lane host's image hash. The server's core module owns core/picture, so ashhart#1502's image.zig, which reaches it by relative path, is compiled from a copy the build writes with those two imports read as core's; the file itself stays byte-identical to ashhart#1502's. tf-xpu-lfm3_vl-test runs one picture without the server (tower, projector, text prefill, decode). Through the server on Iris Xe, greedy: a vaccination record photo, a receipt and an A4 page give the same replies twice (the second from the prompt cache) and with --parallel 2 all at once; a receipt after another picture with the same text equals it on a cold server. LFM3-1.7B's gates do not move: B2's tokens, --parallel 1/2/4 against the pinned solo replies, the cache resume.
…s within 1.7e-3 of float64 Every activation of the tower and the projector stays fp32 (weights bf16 as stored, sums fp32 as before); only the image rows go to the decoder in bf16, as ashhart#1502's rows buffer holds them. Each kernel keeps a rounding flag for transformers' bf16 points (vision.Round, Tower.round; 0 by default): with every bit set the forward is C11's bit for bit. The ablation on ashhart#1502's 14 x 20 fixture against vision_fp64_reference.py in plain float64: no one rounding point carries the gap (any one alone in fp32 leaves the rows at 2.7e-2 to 7.0e-2); taken together they do: the linear outputs, the conv's inner points and the attention path to 7.5e-3, the projector's three points to 3.3e-3, the pixels to 1.65e-3 (layer 13 at 4.3e-6). - fixture: image rows rel 1.65e-3, cos 0.999999 (C11: 4.21e-2, 0.99912); transposed grid 0.82; - the 3000 x 4000 form photo at 1,024 tokens: rel 1.66e-3, cos 0.999999; transposed 1.09. tf-xpu-lfm3_vit-test now runs the kernels with fp32 in and out, within 1e-5 of float64 (at most 1.6e-6), and its controls still fail, GELU's tanh form among them. tf-xpu-lfm3_tower-test dumps fp32 and the first layers' stages by ashhart#1502's trace names; LFM3_VIT_ROUND sets the rounding points.
…ed row for ashhart#1502's compare_logits.py tf-xpu-lfm3_vl_logits-test runs ashhart#1502's real-image fixture (90 tokens, 70 image rows at 4, the suffix "Describe this image." -> "The image contains a cat.") through the tower and the decoder in windows of 16 rows, and writes each row's logits as bf16, the projected rows and the patches. On a stand-in for ashhart#1502's cat (placecats.com/300/200 serves another JPEG now: 14 x 20 patches, 70 rows), against ashhart#1502's fp64_reference.py, compare_logits.py and vision_fp64_reference.py, unchanged: - the XPU's own rows into float64 (decoder alone): rel 1.72e-2 over 90 rows (ashhart#1502: 1.30e-2), 3 top-1 differences, 1 disjoint top set, all at image positions; the suffix: rel 1.17e-2, none; - float64's own vision (whole pipeline): rel 3.74e-2, 3 disjoint top sets over 90 rows (ashhart#1502: 7), all at image positions; the suffix: rel 1.30e-2, no top-1 difference (ashhart#1502: 2 disjoint).
…hes file An optional fifth argument, fp32 [patches][1536] for the picture's grid, replaces the decoded pixels: a receipt's own patches go through the tower and the decoder unchanged, e.g. ashhart#1502's real-image fixture made with the pixel normalization before 50b4e0d (patches sha f1c55a0dd28b as its .npy).
…s within 1.7e-3 of float64 Every activation of the tower and the projector stays fp32 (weights bf16 as stored, sums fp32 as before); only the image rows go to the decoder in bf16, as ashhart#1502's rows buffer holds them. Each kernel keeps a rounding flag for transformers' bf16 points (vision.Round, Tower.round; 0 by default): with every bit set the forward is C11's bit for bit. The ablation on ashhart#1502's 14 x 20 fixture against vision_fp64_reference.py in plain float64: no one rounding point carries the gap (any one alone in fp32 leaves the rows at 2.7e-2 to 7.0e-2); taken together they do: the linear outputs, the conv's inner points and the attention path to 7.5e-3, the projector's three points to 3.3e-3, the pixels to 1.65e-3 (layer 13 at 4.3e-6). - fixture: image rows rel 1.65e-3, cos 0.999999 (C11: 4.21e-2, 0.99912); transposed grid 0.82; - the 3000 x 4000 form photo at 1,024 tokens: rel 1.66e-3, cos 0.999999; transposed 1.09. tf-xpu-lfm3_vit-test now runs the kernels with fp32 in and out, within 1e-5 of float64 (at most 1.6e-6), and its controls still fail, GELU's tanh form among them. tf-xpu-lfm3_tower-test dumps fp32 and the first layers' stages by ashhart#1502's trace names; LFM3_VIT_ROUND sets the rounding points.
…ed row for ashhart#1502's compare_logits.py tf-xpu-lfm3_vl_logits-test runs ashhart#1502's real-image fixture (90 tokens, 70 image rows at 4, the suffix "Describe this image." -> "The image contains a cat.") through the tower and the decoder in windows of 16 rows, and writes each row's logits as bf16, the projected rows and the patches. On a stand-in for ashhart#1502's cat (placecats.com/300/200 serves another JPEG now: 14 x 20 patches, 70 rows), against ashhart#1502's fp64_reference.py, compare_logits.py and vision_fp64_reference.py, unchanged: - the XPU's own rows into float64 (decoder alone): rel 1.72e-2 over 90 rows (ashhart#1502: 1.30e-2), 3 top-1 differences, 1 disjoint top set, all at image positions; the suffix: rel 1.17e-2, none; - float64's own vision (whole pipeline): rel 3.74e-2, 3 disjoint top sets over 90 rows (ashhart#1502: 7), all at image positions; the suffix: rel 1.30e-2, no top-1 difference (ashhart#1502: 2 disjoint).
…hes file An optional fifth argument, fp32 [patches][1536] for the picture's grid, replaces the decoded pixels: a receipt's own patches go through the tower and the decoder unchanged, e.g. ashhart#1502's real-image fixture made with the pixel normalization before 50b4e0d (patches sha f1c55a0dd28b as its .npy).
b32835e to
f965114
Compare
|
Rebuilt on the new #1514 commit ( |
|
@jschmied thank you for running it on the XPU port; the (a) and (b) rows line up with ours. You are right that |
f965114 to
88dd7e6
Compare
|
Rebuilt on the rebased #1514 commit over main |
|
Thanks @BlivionIaG: all three asks from our 1 Nov review are answered in the 10:34 push ( |
zig/src/families/lfm3/image.zig and its test root zig/src/lfm3_image_tests.zig byte-identical to ashhart#1502 at f066644, so they drop out when ashhart#1502 lands.
…ild file of ours zig/build/lfm3_xpu_host.zig declares the step only while no other build file has, so ashhart#1502's own lfm3_host.zig step takes over when it lands; build/sys.zig's walk gives the test root its sys import.
…ainst ashhart#1502's float64 reference xpu_vision.zig loads the 138 bf16 tensors by ashhart#1502's names and shapes and runs one picture's merge-order patches to its gh * gw / 4 image rows: patchifier and embedding norm, the 14 layers (gated depthwise conv or GQA attention with QK norm and axial RoPE, each with the SwiGLU FFN), then the projector (LayerNorm a patch, 2 x 2 blocks as 4096-wide rows, linear_1, GELU, linear_2). zig/src/lfm3_image.zig roots ashhart#1502's image.zig where its relative imports reach. tf-xpu-lfm3_tower-test writes the patches, every layer's rows and the image rows, and the same patches with the grid passed transposed. tools/lfm3/vision_check.py runs ashhart#1502's numpy reference (vision_fp64_reference.py, unchanged) with its bf16 points and in plain float64: - 14 x 20 synthetic picture: image rows rel 4.21e-2, cos 0.99912 against float64 (the reference's bf16 points: rel 6.55e-2); the transposed grid: rel 0.82, cos 0.67; - a 3000 x 4000 photo of a form at 1,024 image tokens (72 x 54 patches): rel 2.84e-2, cos 0.99960 (bf16 points: rel 3.13e-2); transposed: rel 1.09, cos 0.41. The tower takes 164 ms on the synthetic picture and 2.97 s on the photo on Iris Xe.
…ed in place of the image tokens xpu-native reads lfm3_vl checkpoints with the LFM3 family (vl_model_type) and offers image input as request unless --vision-image-tokens names 64 to 4096; LFM3-1.7B still refuses --vision. The engine loads the tower with scratch for that budget. Before a stream's prompt windows, the adapter hands its pictures to the engine (pictures, when the engine has it): the tower writes their rows, and each prompt window copies the rows over the embeddings of the image tokens it holds. Decode, copy drafts, streams and the prompt cache are unchanged; a kept state after a picture is keyed by the lane host's image hash. The server's core module owns core/picture, so ashhart#1502's image.zig, which reaches it by relative path, is compiled from a copy the build writes with those two imports read as core's; the file itself stays byte-identical to ashhart#1502's. tf-xpu-lfm3_vl-test runs one picture without the server (tower, projector, text prefill, decode). Through the server on Iris Xe, greedy: a vaccination record photo, a receipt and an A4 page give the same replies twice (the second from the prompt cache) and with --parallel 2 all at once; a receipt after another picture with the same text equals it on a cold server. LFM3-1.7B's gates do not move: B2's tokens, --parallel 1/2/4 against the pinned solo replies, the cache resume.
…s within 1.7e-3 of float64 Every activation of the tower and the projector stays fp32 (weights bf16 as stored, sums fp32 as before); only the image rows go to the decoder in bf16, as ashhart#1502's rows buffer holds them. Each kernel keeps a rounding flag for transformers' bf16 points (vision.Round, Tower.round; 0 by default): with every bit set the forward is C11's bit for bit. The ablation on ashhart#1502's 14 x 20 fixture against vision_fp64_reference.py in plain float64: no one rounding point carries the gap (any one alone in fp32 leaves the rows at 2.7e-2 to 7.0e-2); taken together they do: the linear outputs, the conv's inner points and the attention path to 7.5e-3, the projector's three points to 3.3e-3, the pixels to 1.65e-3 (layer 13 at 4.3e-6). - fixture: image rows rel 1.65e-3, cos 0.999999 (C11: 4.21e-2, 0.99912); transposed grid 0.82; - the 3000 x 4000 form photo at 1,024 tokens: rel 1.66e-3, cos 0.999999; transposed 1.09. tf-xpu-lfm3_vit-test now runs the kernels with fp32 in and out, within 1e-5 of float64 (at most 1.6e-6), and its controls still fail, GELU's tanh form among them. tf-xpu-lfm3_tower-test dumps fp32 and the first layers' stages by ashhart#1502's trace names; LFM3_VIT_ROUND sets the rounding points.
…ed row for ashhart#1502's compare_logits.py tf-xpu-lfm3_vl_logits-test runs ashhart#1502's real-image fixture (90 tokens, 70 image rows at 4, the suffix "Describe this image." -> "The image contains a cat.") through the tower and the decoder in windows of 16 rows, and writes each row's logits as bf16, the projected rows and the patches. On a stand-in for ashhart#1502's cat (placecats.com/300/200 serves another JPEG now: 14 x 20 patches, 70 rows), against ashhart#1502's fp64_reference.py, compare_logits.py and vision_fp64_reference.py, unchanged: - the XPU's own rows into float64 (decoder alone): rel 1.72e-2 over 90 rows (ashhart#1502: 1.30e-2), 3 top-1 differences, 1 disjoint top set, all at image positions; the suffix: rel 1.17e-2, none; - float64's own vision (whole pipeline): rel 3.74e-2, 3 disjoint top sets over 90 rows (ashhart#1502: 7), all at image positions; the suffix: rel 1.30e-2, no top-1 difference (ashhart#1502: 2 disjoint).
…hes file An optional fifth argument, fp32 [patches][1536] for the picture's grid, replaces the decoded pixels: a receipt's own patches go through the tower and the decoder unchanged, e.g. ashhart#1502's real-image fixture made with the pixel normalization before 50b4e0d (patches sha f1c55a0dd28b as its .npy).
zig/src/families/lfm3/image.zig and its test root zig/src/lfm3_image_tests.zig byte-identical to ashhart#1502 at f066644, so they drop out when ashhart#1502 lands.
…ild file of ours zig/build/lfm3_xpu_host.zig declares the step only while no other build file has, so ashhart#1502's own lfm3_host.zig step takes over when it lands; build/sys.zig's walk gives the test root its sys import.
…ainst ashhart#1502's float64 reference xpu_vision.zig loads the 138 bf16 tensors by ashhart#1502's names and shapes and runs one picture's merge-order patches to its gh * gw / 4 image rows: patchifier and embedding norm, the 14 layers (gated depthwise conv or GQA attention with QK norm and axial RoPE, each with the SwiGLU FFN), then the projector (LayerNorm a patch, 2 x 2 blocks as 4096-wide rows, linear_1, GELU, linear_2). zig/src/lfm3_image.zig roots ashhart#1502's image.zig where its relative imports reach. tf-xpu-lfm3_tower-test writes the patches, every layer's rows and the image rows, and the same patches with the grid passed transposed. tools/lfm3/vision_check.py runs ashhart#1502's numpy reference (vision_fp64_reference.py, unchanged) with its bf16 points and in plain float64: - 14 x 20 synthetic picture: image rows rel 4.21e-2, cos 0.99912 against float64 (the reference's bf16 points: rel 6.55e-2); the transposed grid: rel 0.82, cos 0.67; - a 3000 x 4000 photo of a form at 1,024 image tokens (72 x 54 patches): rel 2.84e-2, cos 0.99960 (bf16 points: rel 3.13e-2); transposed: rel 1.09, cos 0.41. The tower takes 164 ms on the synthetic picture and 2.97 s on the photo on Iris Xe.
…ed in place of the image tokens xpu-native reads lfm3_vl checkpoints with the LFM3 family (vl_model_type) and offers image input as request unless --vision-image-tokens names 64 to 4096; LFM3-1.7B still refuses --vision. The engine loads the tower with scratch for that budget. Before a stream's prompt windows, the adapter hands its pictures to the engine (pictures, when the engine has it): the tower writes their rows, and each prompt window copies the rows over the embeddings of the image tokens it holds. Decode, copy drafts, streams and the prompt cache are unchanged; a kept state after a picture is keyed by the lane host's image hash. The server's core module owns core/picture, so ashhart#1502's image.zig, which reaches it by relative path, is compiled from a copy the build writes with those two imports read as core's; the file itself stays byte-identical to ashhart#1502's. tf-xpu-lfm3_vl-test runs one picture without the server (tower, projector, text prefill, decode). Through the server on Iris Xe, greedy: a vaccination record photo, a receipt and an A4 page give the same replies twice (the second from the prompt cache) and with --parallel 2 all at once; a receipt after another picture with the same text equals it on a cold server. LFM3-1.7B's gates do not move: B2's tokens, --parallel 1/2/4 against the pinned solo replies, the cache resume.
…s within 1.7e-3 of float64 Every activation of the tower and the projector stays fp32 (weights bf16 as stored, sums fp32 as before); only the image rows go to the decoder in bf16, as ashhart#1502's rows buffer holds them. Each kernel keeps a rounding flag for transformers' bf16 points (vision.Round, Tower.round; 0 by default): with every bit set the forward is C11's bit for bit. The ablation on ashhart#1502's 14 x 20 fixture against vision_fp64_reference.py in plain float64: no one rounding point carries the gap (any one alone in fp32 leaves the rows at 2.7e-2 to 7.0e-2); taken together they do: the linear outputs, the conv's inner points and the attention path to 7.5e-3, the projector's three points to 3.3e-3, the pixels to 1.65e-3 (layer 13 at 4.3e-6). - fixture: image rows rel 1.65e-3, cos 0.999999 (C11: 4.21e-2, 0.99912); transposed grid 0.82; - the 3000 x 4000 form photo at 1,024 tokens: rel 1.66e-3, cos 0.999999; transposed 1.09. tf-xpu-lfm3_vit-test now runs the kernels with fp32 in and out, within 1e-5 of float64 (at most 1.6e-6), and its controls still fail, GELU's tanh form among them. tf-xpu-lfm3_tower-test dumps fp32 and the first layers' stages by ashhart#1502's trace names; LFM3_VIT_ROUND sets the rounding points.
…ed row for ashhart#1502's compare_logits.py tf-xpu-lfm3_vl_logits-test runs ashhart#1502's real-image fixture (90 tokens, 70 image rows at 4, the suffix "Describe this image." -> "The image contains a cat.") through the tower and the decoder in windows of 16 rows, and writes each row's logits as bf16, the projected rows and the patches. On a stand-in for ashhart#1502's cat (placecats.com/300/200 serves another JPEG now: 14 x 20 patches, 70 rows), against ashhart#1502's fp64_reference.py, compare_logits.py and vision_fp64_reference.py, unchanged: - the XPU's own rows into float64 (decoder alone): rel 1.72e-2 over 90 rows (ashhart#1502: 1.30e-2), 3 top-1 differences, 1 disjoint top set, all at image positions; the suffix: rel 1.17e-2, none; - float64's own vision (whole pipeline): rel 3.74e-2, 3 disjoint top sets over 90 rows (ashhart#1502: 7), all at image positions; the suffix: rel 1.30e-2, no top-1 difference (ashhart#1502: 2 disjoint).
…hes file An optional fifth argument, fp32 [patches][1536] for the picture's grid, replaces the decoded pixels: a receipt's own patches go through the tower and the decoder unchanged, e.g. ashhart#1502's real-image fixture made with the pixel normalization before 50b4e0d (patches sha f1c55a0dd28b as its .npy).
88dd7e6 to
cdc606b
Compare
|
Rebased onto the new #1514 commit over main |
|
Thanks @BlivionIaG. The rebuild onto the new #1514 commit looks right, with the vision and speed commit applying cleanly. Nothing more is needed from you. As before, it lands squashed after #1514, once our M5 Ultra and M3 Ultra VL runs are in. On the M5 Ultra, those include |
…ation, snapshots and contract probes with planted controls Serves the official MLX 4-, 6- and 8-bit LFM3-1.7B checkpoints through the native lane engine. LFM3-VL checkpoints are refused here; vision and the tensor-unit prompt route stack on this change. Refs ashhart#1491.
…and stepped prefill Adds the native image tower and projector for LFM3-VL-2B, the opt-in tensor-unit prompt route (tf_qmmw_t_nax_win), the tiled prompt attention and key-parallel decode attention, and stepped prefill that takes the lane host's row cap. The tensor-unit options are off in every build. Refs ashhart#1491.
cdc606b to
e1e8247
Compare
|
Rebuilt on the new #1514 commit over main |
What changes
Native Metal image preparation, vision encoder and projector for LFM3-VL-2B from the official MLX 4/6/8-bit checkpoints. Projected image rows feed the LFM3 decoder and the existing lane engine (image order and content handling, prompt caching, verification, snapshots, concurrent serving). Python is used only for development references and clients.
Refs #1491. Stacked on admission #1513 (merged) and text #1514 (vision-only diff: compare). The threaded-reference and GLM work that used to sit under #1514 lives in #1776.
This PR also carries the LFM3 prompt and decode speed work, because the VL prompt and the text prompt share it. The tensor-unit routes are opt-in:
lfm3-nax-prompt,lfm3-tiled-attentionand 2048-row chunks are off in every build, so a source build and a release serve a checkpoint with the same arithmetic. Every speed number below is forzig build native -Dcpu=apple_m1 -Dlfm3-nax-prompt=true -Dlfm3-tiled-attention=true; a default build keeps the row kernels and is as slow as the text slice (#1514 receipt).lfm3-nax-prompt: every dense prompt projection (attention q/k/v/out, convolution in/out, FFN gate/up/down) runs on the tensor units throughtf_qmmw_t_nax_win, Flash Next'stf_qmmw_t_naxtile with scales and biases loaded eight groups at a time (same output bytes, about 4% faster). Weights stay packed (4/6/8-bit, group 32 or 64) and are dequantized to BF16 per 64-wide K step in threadgroup memory, as MLX does; peak memory (text, 8k tokens) is the packed weights plus a width-independent 0.68 GB.lfm3-tiled-attention: prompt segments of 9–2048 rows are one tiled launch (64-query/32-key tiles, FP32 online softmax, probabilities rounded once to BF16 for PV, as MLX does). Decode windows, shared decode rows and prompt tails of at most 8 rows use a key-parallel split kernel (≤64 parts of ≥128 keys, FP32 merge). Both replay through the exact path when a row is nonfinite.prefill_step): the backend fills a prompt one chunk at a time and returns to the lane engine between chunks (prefill_step(ptr, stream, max_rows): 0 is the engine's own cut), so the engine's shortest-remaining-first policy and decode-between-chunks apply to LFM3. Cuts do not change state (tf-lfm3-nax-exact), so the backend declareschunk_invariantand--decode-sharemay cap chunks at 512 rows. A request cancelled between chunks releases its fill.-Dlfm3-nax-prompt=true -Dlfm3-tiled-attention=trueselects them; the residual-weight FFN experiment is removed.Performance against mlx-vlm
One M5 Ultra, same checkpoints and prompts,
--prompt-cache-gib 0, medians of 2–3 runs after a warm-up, opt-in build as above. Receipts: cold prefill, serving metrics, decode share.Cold prefill TTFT, native / mlx-vlm (below 1 is faster):
Serving, text 4-bit, native vs mlx-vlm (default scheduling, no flags):
Across the six checkpoints: single-stream decode is 1.10–1.14× mlx-vlm at short context, 1.26–1.28× at 2k and 1.17–1.20× at 8k. Four-stream aggregate decode is 1.11–1.18× at 2k and 1.10–1.25× at 8k, and 0.93–1.00× at short context. Four-stream TTFT per request is 0.70–0.73× at 2k and 0.74–0.77× at 8k.
Open against mlx-vlm:
--decode-sharecloses the ITL gap at a cost. With chunks capped at 512 rows while replies decode (receipt, text 4-bit, four requests, 2k / 8k):--decode-share 0.5--decode-share 0.75With the flag, mean and p95 ITL and aggregate decode beat mlx-vlm at both lengths; the price is aggregate prefill and 2k TTFT.
serve_check --cancel --sampledwith--decode-share 0.5passes 40 checks per text width and 61 per VL width, so the cap changes no tokens.Fidelity (policy of #1491)
Independent FP32 forward and official MLX BF16 as references. Receipt. No pass rule is applied here; the differences are listed.
Kernel contract
New kernels are registered in
zig/src/core/contract/registry.zigandlfm3_records.zig:tf_qmmw_t_nax_winprobes for q4g64, q6g64, q8g64, q4g32 (nonfinite, repeat, shapes, sums; planted bad-bias and bad-nonfinite controls red) and the split attention (full and sliding; bad-tail and bad-nonfinite controls red). The 11 image-tower kernels each have a planted control (metal_lfm3_vision_controls.zig), and so do the 16 base text probes (metal_lfm3_controls.zig, in #1514). Local--only LFM3on an M4 Pro:SUMMARY 74 kernels: 74 pass gates 1-4, 0 fail, 0 known findings; 52 controls: 52 red as planted, 0 missed. Full M5 Ultra run on this head (tf-kernel-contract --rotary-accuracy rotary.json):SUMMARY 468 kernels: 468 pass gates 1-4, 0 fail, 3 known findings; 420 controls: 420 red as planted, 0 missed; 0 limit problems. The three known findings are twoaffine_mminfinity classes and the Nemotronrelu2NaN; none is an LFM3 kernel. Receipt.Checks
zig build test test-golden dev-tools test-lfm3-host -Dcpu=apple_m1passes;zig fmt --check build.zig zigpasses;lean_check.pyreports 425 problems, the same as main. Licence: the official LFM3 exports arelicense: other(lfm1.0); the recipe says so. Macs checked: an M5 Ultra only.Gates from #1491
Receipts: exactness, serving, image rows, 32k.
tf-lfm3-nax-exact): 43 byte-equality cases pass at all six widths on this head: prompts cut at 1 to 2047 rows and at five cut sets equal 2048-row chunks, resuming from a saved prefix equals a fresh run, and verification windows of 2 to 32 rows equal one-row decode. The check found one bug, fixed in this PR: windows of 9 or more rows ran the tiled prompt attention, so they differed from decode (9 of 43 cases failed). Windows now use the decode kernel.serve_check.py --cancel --sampledon this head passes 40 checks per text width and 61 per VL width with 0 failures (drafted/plain, concurrent 1/2/4 vs solo, cache reuse, image order and content, cancel then recover). With 31k-token prompts at--context 40000, a request cancelled mid-prefill leaves the server answering the next request in 42–54 ms with the same output as fresh, and a short request sent while three 31k prompts prefill is answered in 0.67–0.74 s (3.3–3.5 s before prefill was stepped) while the three prompts still finish in 4.7–4.9 s.real-image-fp64-input.png.Dense GEMM: what was measured and not adopted
Isolated 4-bit prompt GEMM at M=2048 on the M5 Ultra, TFLOPs (the new windowed-scale kernel against the previous one and MLX's
qmm4; the outputs of the first two are byte-identical on four shapes):tf_qmmw_t_nax_wintf_qmmw_t_naxIn the model the dense layers run at the same efficiency as this bench (24 layers; no unexplained gap), and a ring of cold weights shows no loss, so weight-from-DRAM latency is not the limit. Tried and rejected, each measured against the same baseline: 64x128 and BM 128/192/256 tiles (no better, or register spills); the SWZ swizzle (neutral once scales are windowed); a 128-deep K step (+4% on wide shapes, -7% on square ones); register prefetch of four weight steps (-2 to -7%); threadgroup staging of weights (-2 to -14%); a fused gate+up+SwiGLU GEMM (13% slower than two GEMMs); fusing SwiGLU and the paired q/gate projection into the decode dispatches (+1.1 to +1.6% on one stream, -1.6 to -2.0% on four; receipt).
Open
The prefill and four-stream ITL gaps against mlx-vlm described above. Fusing the FFN SwiGLU and the attention query/gate projections into one dispatch each (26 fewer of about 250 dispatches per decoded token) gained 1–2% on one stream and lost 2% on four; not adopted (receipt). The pass rule for making the tensor-unit route the default is on #1491.