Repository navigation
Unlimited-OCR-2 on CUDA: the family's steps on NVIDIA GPUs, the router's long-context speedup (stacked on #2074) - #2077
BobClawblaw wants to merge 36 commits into
Conversation
|
Thanks @BobClawblaw. Porting all 28 steps with a five-group contract, and planting 33 controls that all go red, is thorough. The router change also keeps its picks bit for bit. This waits on #2074, so we'll review it fully once #2074 lands. Before then:
|
|
Thanks. On 3: #2074 adds no findings. On Linux, its merge base ( On 1 and 2: we'll rebase #2074 and this onto main, then post the CUDA hashes for the five pages and the 45-page document next to your Metal ones, plus the reference's top-two margins at each first differing id. |
|
Thanks @BobClawblaw, that's a real find. |
|
Rebased both onto main ( 1. CUDA against Metal. I ran both sides on the rebased head with the same page PNGs: Metal on an M5 Ultra, CUDA on an RTX 5090 (sm_120, Linux). Each engine repeats its own bytes exactly, but CUDA does not equal Metal:
The differences start at box coordinates one or a few grid points apart, and paper-3 is the 2. Margins against the torch reference. This is the release's own
paper-10 and report-2 equal the reference exactly. The x86 summary on this build is the same as the 4080's: text without boxes identical in 5 of 6, box coordinates moved 13 of 284, by at most 2. 3. As above: no new findings. On Linux the rebased head reads 425, the same as main. Checks on the rebased head: |
SAM ViT-B, its neck and CLIP-L to the projector's rows, for the 1024 global view and 640 tiles (position tables resized as torch does, bit-identical). tf-uocr-vision gates each stage against float32 on a reference dump's pixels.
…oE) and greedy generation tf-uocr-decode runs a page end to end against a reference dump: the prompt's ids, the spliced embeddings, each layer's prefill rows against float32, the first logits, and the greedy reply.
…matvecs for one token 49 -> 142 tok/s on a one-page reply; the two-page reply now equals torch bf16's over all 337 tokens.
Prompt rows always take the GEMM (an expert's lone row in a chunk took the matvec before), so a row's bits do not depend on the chunking; tf-uocr-decode --chunk ROWS checks the first logits and the reply.
… template in the family folder) One request at a time on the lane host, greedy under the page guard. A page takes gundam tiles when they fit its share of the image tokens (the publisher's server default), else the base view.
…ernel groups, their records)
…ery reply token fed back The server clamps a reply to the room the prompt leaves; the family refused that reply by one row, so any request without max_tokens failed once --max-tokens reached the context.
…ne-line comments, the records' duplicate test dropped
… none; --chat-template still overrides
…nd that it runs on M5 GPUs
…built-in template
0632249 to
029369d
Compare
…ld-native uocr.cu ports uocr/vision.metal's and uocr/decoder.metal's steps op for op, rounded at the same points; cuBLASLt (cuda/vit.zig's Gemms) runs the projections. The CUDA host (cuda_weights, cuda_vision, cuda_decoder, cuda_serve) mirrors the Metal one: one device buffer for the bf16 weights, matvecs and on-GPU expert picks for a decoded token, the greedy pick under the page guard on the GPU. native/uocr_cuda.zig opens it beside the registry families. The host files stay shared: preprocess.With takes the picture decoder (core's on CUDA, a file belongs to one module), and the page guard moves to guard.zig so the CUDA module does not reach the Metal decoder. On an RTX 4080 SUPER (Windows 11): the six OCR cases' text matches the publisher's torch reference (one near-tie on a page alone, `$ [5] $` vs `[5]`, which the Mac's run takes the other way), 236-242 tok/s on a page, first token 0.35-0.46 s, and a 45-page document (40,685 prompt tokens) runs to its last page in a 98,304-token window.
… then merged A decoded token's router read every block's summary key with one warp, then took the top 16 in 16 rounds: its time grew with the context (140 tok/s at 41k tokens against 238 at 1k on an RTX 4080 SUPER). Each block's score is now a thread's (the same sequential sum), each warp keeps its own top 16 by shuffles, and warp 0 merges the eight lists in the one order (value, then the lower block), so the reads and their biases are bit for bit what they were. RTX 4080 SUPER, 1,500 decoded tokens: 224 tok/s after a 40,685-token prompt (was 140), 235 after 13,565 (was 192), 242 after 909; the 45-page document's 43,027-token reply 222 tok/s (was 118), byte-identical to before.
…ee headers of at most 600 lines
…, their records and control headers)
…m_80, 86, 89, 120 and 121; 13.0 pending)
…indow_add, rel_pos, concat, clip_embed, gather, scatter, embed)
… MoE gate and combine, greedy pick)
…on as SAM windows, SAM global and CLIP; tf_uocr_attend), 5 controls
…eir 13.3 usage checked; the lean rule's 600 lines kept
…d, so its errdefers release the host, context and driver
… stream, as the Metal engine does
029369d to
e1e345a
Compare
What this changes
Unlimited-OCR-2 (#2074's
unlimited-ocrfamily) gains a CUDA backend:tensorfold-native serve --visionon an NVIDIA GPU serves page images with the release's BF16 weights as published, one stream, greedy under the publisher's page guard, as the Metal engine does. Before this, the family served on Metal only and CUDA refusedunlimited-ocrcheckpoints.86857ca4b, on mainb331731e6). This adds:46e00f7d1the CUDA port;e1d02d708the router's long-context speedup;f385194e2the lean rule;f9e0e9b88to113b73040the kernel contract: the scaffold, the five groups and the launch records;cf52f953ca fix to the CUDA open: its refusals after the host is made (no driver, a checkpoint it cannot load) returned null past its errdefers, leaking the host (42,592 bytes) and, on the load path, the context and driver. They now returnerror.Refusedasnative/gemma4.zig's do; a checkpoint that fails to load no longer reports a leak;e1e345a25a refusal of a named--parallelabove one ("Unlimited-OCR-2 serves one stream: serve it without --parallel"), as Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074's Metal engine refuses it, with a test. Before it, CUDA served one stream whatever--parallelasked.atc overlap --jsonon this head: incomplete (pullFilesUnavailable,timeLimit). It lists 432 open pull requests sharing files, almost all throughCHANGELOG.md, the contract registry andzig/build/cuda.zig: 6 with textual conflicts (server: render DeepSeek-V4.x chat with its own encoder and schema prompt #1698, Clef-Flash: joint-schema decisions on /v1/systemone (HIP) #2048, fix(picture): compare PNG tRNS keys by their low byte #2100, server: api.Structure keeps the name a refusal gives it (#2084) #2110, qwen27: canonical prompt chains follow the admitted frame on pre-M5 chips, past 128 rows #2120, glm metal: one arithmetic order for prompt windows, chunks and decode on M1-M4 (#743) #2128), 4 clean, the rest not checked. None touchesfamilies/uocrexcept Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074, which this is stacked on.How it is built:
zig/kernels/cuda/uocr.cu(withuocr_common.cuh,uocr_vision.cuhanduocr_decoder.cuh) holds the Metal family's 28 own steps, each rounded at the same points as its Metal kernel:The projections run on cuBLASLt through
cuda/vit.zig'sGemms(the bias in its epilogue, or fp32 sums and one rounding), as MOSS-Image's CUDA path does. They sum in cuBLASLt's order, not Metal's, so the replies are not bit-equal to the Metal engine's: they part at near-ties (Exactness, below).families/uocr/cuda_*.zigandnative/uocr_cuda.zigmirror the Metal host: the weights in one device buffer, the encoder, the decoder and the served model, opened fromnative/cuda.zigbeside the registry families.The family's host files are shared rather than copied. A file may belong to only one module, so two things move:
preprocess.With(picture)takes the picture decoder: core/picture through its relative path on Metal, thecoremodule's on CUDA;guard.zig(generate.zigre-exports it), so the CUDA module never reaches the Metal decoder.The CUDA module is rooted at
zig/src/uocr_cuda.zigfor the same reason.The router change: a decoded token's router scored every 32-key block of the context with one warp, then took the top 16 in 16 rounds, so decode slowed as the context grew. It is now a 256-thread block. Each block's score is one thread's sum, in the same order as before. Each warp takes its own top 16 by shuffles, and warp 0 merges the eight lists in the same order: value first, then the lower block. The reads, their biases and the replies are bit for bit what they were.
Checks on this head
e1e345a25.python3 tools/zig/lean_check.pyon Linux and macOS: 425 problems on main (b331731e6), 425 on Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074's head and 425 on this head. New findings: 0;zig/build/cuda.zigandregistry.zigstay at 600 lines. (On Windows the checker reads files as cp1252 and counts 15 more on every tree.)zig build test-cuda-host -Dsm=120 -Dnvcc=...:Build Summary: 307/307 steps succeeded; 1183/1184 tests passed (1 skipped): zig re-ran the test binaries this head changes and took the rest from its cache of029369d16's full run (4748/4769, 21 skipped). The new refusal test fails with the check disabled. The built server refusesserve --vision --parallel 2with that message. In the full run three passing steps print to stderr and so show afailed command:line (Dory RoPE's word table, a gfx1030 N/A line, the libtfgrammar warning); none is this change. No leak reports.zig build native install -Dsm=120 -Dnvcc=...builds.tf-cuda-test uocr-contracton029369d16(the same kernels and contract files as this head):SUMMARY uocr: 35 kernels, 35 pass gates 1-4, 0 fail; 33 controls: 33 red as planted, 0 missed.zig fmt --check build.zig zig,zig build test,test-golden,test-kernel-contract,test-server-security test-pull-security(24 passed, 0 failed),check-release-paths,fuzz-cortheon,dev-toolsandzig build native install: all pass oncf52f953c(this head's parent; the last commit changes only CUDA files) and on Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074's head. No leak reports.06322491fon Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074's10fe5c459), Windows 11, RTX 4080 SUPER:test-cuda-host -Dsm=89285/285 steps;tf-cuda-test uocr-contractgave the same SUMMARY on sm_89.Receipt
e1e345a25), except where marked as the RTX 4080 SUPER's earlier base.baidu/Unlimited-OCR-2at revision0480966938291445591f69f08a13b041c073a96a.ocr-matchplan on the same page PNGs: the recipe's five pages alone, the five as one document, and the release's 15-page paper three times over as one 45-page request.Against the Metal engine (M5 Ultra, Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074's code on this head): equal on the two report pages, different on the other five cases. Each engine repeats its own bytes exactly.
58c9ad432da973e458c9ad432da973e4366f37903b4eec02366f37903b4eec029211a96c01c0dd849960608a870adef444da612e99c36206357d7be06ce47b968d377476d6ea03c666767fb282460f00f7fc6f672d7c67d1a5acde0bcdabd6d13904790bb7f0ef286e4269decbf659acAgainst the release's own
infer(modeling_unlimitedocr.py) on the CPU in torch bf16 (x86, torch 2.13.0+cpu, transformers 4.52.1), greedy, gundam mode: the text without layout boxes is identical in 5 of 6 cases; box coordinates moved in 13 of 284, by at most 2 on the 0-999 grid (the RTX 4080 SUPER on the earlier base: 13 of 284, by at most 5). report-2 and paper-10 are identical.Every place the CUDA reply leaves the reference is a near-tie in the reference. Driving the reference along the CUDA reply and recording its logits after the page guard wherever its own pick (
argmax, the lowest id on a tie) differs: 9 such steps over the four cases, and at each the CUDA token is the reference's second choice, either an exact tie (4) or one bf16 step below (0.125 or 0.25, 5). The sixth case's text difference (paper-3,$ [5, 6, …] $against[5, 6, …]) is one of them: 24.50 against 24.375.A repeated request gives the same bytes, and two requests at once give their solo replies (one stream: they queue). There are no drafts, so there is no
"draft": falsecomparison.decode_tps;tools/bench_openai.pynot run):tools/bench_cache.pywas not run: the family has no prompt cache.Kernel contract, only when a GPU kernel is new or changed
tf_uocr_*kernels inuocr.cu. None replaces a CUDA kernel. The command istf-cuda-test uocr-contract, with five groups that mirror Unlimited-OCR-2 on native Metal: the publisher's BF16 release, page images to structured text #2074's Metal groups (zig/tests/cuda/contract_uocr_*.zig):nan_to_num), silu_mul;sqrtfcorrectly rounded;expfwithin 2 ulps andlogfwithin 1;zig/kernels/cuda/contract_uocr_*.cuh(each a copy of the production kernel with one break), all red as planted:SUMMARY uocr: 35 kernels, 35 pass gates 1-4, 0 fail; 33 controls: 33 red as planted, 0 missed. There are no known findings. The tightest passes:uocr_cuda_*_records.zig, amongcuda_pending. Their nvcc 13.0 usage is pending, because only nvcc 13.3 was available. 13.3's usage on sm_80, sm_86, sm_89, sm_120 and sm_121 is inmeasurements/uocr_133.zig, fromtf-cuda-test kernel-contract-limitson-Dsm=80,86,89,120,121fatbins: every launch PASS on every SM. The registry's host test checks that usage against all five SMs.kernel-contract-limitsrun then stops atdsv41_x3ld's sm_121a-only kernel (SymbolNotInImage): this build has no sm_121a image. That is not this change.Checklist
"draft": false(no drafts), a resumed prompt equals a fresh one (no prompt cache), and concurrent equals solo.