# Tabularium — registro test locali e benchmark Registro operativo dei test eseguiti per verificare il workflow reale: `Registro modelli → download → serving locale → endpoint OpenAI-compatible → inferenza`. Il benchmark è uno strumento interno di verifica. Non è una funzione che l’utente deve configurare o usare per annotare dati, e nessun output del benchmark viene salvato come annotazione. ## Regole del test - Un solo VLM alla volta sulla GPU locale. - Stessa immagine, stesso task e stesso `max_pixels` quando si confrontano modelli. - Si registra sempre adapter, checkpoint, runtime, porta, latenza e risultato. - Download e serving sono verificati attraverso le stesse API che usa la UI; i comandi shell servono soltanto per raccogliere evidenza tecnica. - Un test non eseguito non viene marcato come riuscito. - Le misure su una GPU già occupata non sono confrontabili e vanno ripetute. ## Hardware e runtime — 2026-09-01 | Voce | Evidenza | |---|---| | GPU | NVIDIA GeForce RTX 4060 Laptop GPU | | VRAM | 8188 MiB | | Compute capability | 8.9 | | Driver | 580.173.02 | | PyTorch | 2.13.0+cu130 | | CUDA visibile a PyTorch | sì, 1 dispositivo | | vLLM | 0.28.0 | | Transformers | 5.16.1 | | Hugging Face Hub | 1.29.0 | | Docker | 27.1.1 | La prima interrogazione `nvidia-smi` dentro il sandbox non vedeva il driver; la verifica diretta successiva ha confermato GPU e CUDA funzionanti. ## Stato dei checkpoint locali Verificato il 2026-09-01 con `du -sh data/models/*`: | Adapter | Checkpoint locale | Dimensione osservata | Download | Serving/inferenza | |---|---:|---:|---|---| | `monkeyocrv2-parsing` | sì | 2.0 GB | presente | layout superato | | `mineru2.5` | sì | 2.2 GB | presente | text superato; layout da riallineare | | `paddleocr-vl` | sì | 1.9 GB | presente | text superato | | `deepseek-ocr` | sì | 6.4 GB | presente | text superato; layout non delimitato | | `unlimited-ocr` | sì | 6.4 GB | presente | end2end superato | | `dots-ocr` | temporaneo | 5.7 GiB in `/tmp` | download riuscito | OOM anche con contesto 4096/offload/eager | | `glm-ocr` | temporaneo | 2.5 GB in `/tmp` | download riuscito | engine non completa warmup anche a contesto 4096 | | `qwen3-vl-8b` | parziale | 204 MiB | resume UX fermato a 212.8 MB; stima 16.3 GB | non verificato | È presente anche il draft `monkeyocrv2-parsing-draft` (161 MB), ma è marcato `.unusable` dopo il tentativo precedente: sulla 4060 il serving deve ripiegare sul modello senza DFlash. ## Test UX e prodotto ### Frontend — superato Eseguito il 2026-09-01: ```text npm test -- --run Test Files 13 passed (13) Tests 79 passed (79) ``` Build produzione superata: ```text npm run build vite build: success ``` Il detector UI sui componenti modificati non ha rilevato problemi: ```text node .../impeccable/scripts/detect.mjs --json \ frontend/src/app/ModelsModal.tsx frontend/src/pages/EvaluationPage.tsx [] ``` La UI consente di scaricare e avviare un modello dal Registro. La porta locale è selezionabile perché una porta occupata non deve costringere l’utente a modificare codice. ### Backend e benchmark — compilazione superata ```text PYTHONPATH=backend data/vllm-runtime/bin/python -m py_compile \ backend/app/services/inference.py backend/app/services/evaluate.py \ backend/app/api/evaluate.py scripts/benchmark_models.py backend-and-benchmark-checks-passed ``` Il Python di sistema non contiene le dipendenze dev (`httpx2`), ma la suite è stata eseguita con `backend/.venv/bin/pytest`: ```text backend/.venv/bin/pytest -q backend/tests/test_model_registry.py 28 passed in 1.15s ``` Questo include il test del preflight: con 2 GiB liberi il download GLM viene rifiutato prima di creare la directory o avviare un sottoprocesso. La suite backend completa è stata poi eseguita con accesso ai socket locali necessari ai test di lifecycle del serving: ```text backend/.venv/bin/pytest -q backend/tests 292 passed, 3 skipped, 1 deselected in 68.42s ``` Il test escluso è `test_provider_refresh_persists_rate_and_live_state`, legato a una rotta mock Vast.ai (`/api/v1/instances/`) e non al registry, OAuth, serving locale o benchmark. ## Test di serving/inferenza ### Unlimited-OCR — deploy e inferenza end-to-end superati Il 2026-09-01 era attivo un container Docker con: ```text vllm/vllm-openai:unlimited-ocr --served-model-name Unlimited-OCR --port 8888 --max-model-len 12288 --gpu-memory-utilization 0.85 --cpu-offload-gb 4 ``` Health check osservato: ```json {"object":"list","data":[{"id":"Unlimited-OCR"}]} ``` VRAM osservata durante il serving: circa 5646 MiB usati. Il primo lancio del benchmark aveva un bug nel runner (immagine non passata a `end2end`), corretto subito dopo. La seconda prova ha poi evidenziato un secondo bug: il retry generico per liste JSON veniva applicato anche al formato grounded-markdown di Unlimited-OCR. Il client ora riconosce il protocollo nativo e non ritenta inutilmente. | Task | Esito | Wall | TTFT | Output | |---|---:|---:|---:|---| | end2end | superato, 4/4 blocchi validi | 29.375 s | 1.929 s | `data/benchmarks/bench_20260901T173836Z/outputs/unlimited-ocr/end2end-001.json` | Il container è stato fermato dopo la prova e la GPU è tornata libera. ### Spazio disco — blocker operativo rilevato Dopo i checkpoint già presenti, la partizione del progetto ha raggiunto circa il 100% di utilizzo, con 1.3 GB liberi al controllo del 2026-09-01. Il download di GLM-OCR (file principale atteso ~2.65 GB) è stato arrestato da Hugging Face con `No space left on device`; sono rimasti soltanto 6.7 MB di metadati nella cartella `data/models/glm-ocr`. Qwen3-VL è stimato a 16.3 GB e non può essere scaricato in queste condizioni. Nessun file esistente è stato cancellato. ### MonkeyOCRv2 — avvio e layout superati Il tentativo precedente sulla porta 8888 era stato impedito dal container Unlimited-OCR già attivo; non era un fallimento del modello. Aggiornamento 2026-09-01: prova completata su GPU libera con il runtime dedicato. Il server è diventato pronto dopo il caricamento e il warmup; il processo ha usato `--gpu-memory-utilization 0.9`, `--max-model-len 24576` e ha esposto il modello `MonkeyOCRv2`. Il risultato è stato poi salvato e il server è stato fermato correttamente. | Task | Immagine | Esito | Wall | TTFT | Output | |---|---|---:|---:|---:|---| | layout | `test/1502-a-BANCO-SAN-GIORGIO-originale.jpg` (741×1024) | 5/5 elementi validi | 2.537 s | 1.183 s | 147 token, 165.11 token/s | Artefatti completi: - report: `data/benchmarks/bench_20260901T144611Z/report.json` - raw output: `data/benchmarks/bench_20260901T144611Z/outputs/monkeyocrv2-parsing/layout-001.json` Nota: questa è una misura di serving/layout e non una metrica di qualità gold. Per qualità servono le pagine annotate e il validation split. Il 2026-09-02 il checkout ufficiale è stato ripristinato automaticamente in `data/vendor/MonkeyOCRv2` e il workflow completo `scripts/serve_model.sh` → `parsing/serve.py` è stato ripetuto con path assoluti, come fa la UX. Il server ha esposto `MonkeyOCRv2`; il benchmark omogeneo `text` ha prodotto 916 caratteri validi in 3.628 s (TTFT 0.758 s, 164.80 token/s). Output: `data/benchmarks/bench_20260902T064827Z/outputs/monkeyocrv2-parsing/text-001.json`. ### MinerU2.5 — deploy superato, protocollo da riallineare La verifica dalla UX aveva già dimostrato che MinerU si avvia e funziona nel prefill. La ripetizione del test sul percorso `serve_manager` ha confermato: - caricamento del checkpoint completato; - endpoint `/v1/models` pronto; - GPU utilizzata durante il serving senza OOM; - inferenza testuale non vuota sulla stessa immagine campione. | Task | Esito | Wall | Output | |---|---:|---:|---| | layout | HTTP OK, ma 0 blocchi / 1 token | 1.217 s | `data/benchmarks/bench_20260901T145521Z/outputs/mineru2.5/layout-001.json` | | text | superato, 1116 caratteri | 3.352 s | `data/benchmarks/bench_20260901T150055Z/outputs/mineru2.5/text-001.json` | Il risultato layout non invalida il deploy: è un’anomalia di compatibilità fra il runner/parser e il protocollo layout MinerU, perché lo stesso modello è già usabile dalla UX. Va quindi corretto il benchmark per riprodurre esattamente il percorso UX prima di confrontare i modelli. Il test iniziale fatto mentre il server non era ancora pronto (`data/benchmarks/bench_20260901T145212Z/report.json`) resta registrato come errore di readiness, non come errore del modello. ### PaddleOCR-VL — deploy e inferenza superati Il 2026-09-01 il checkpoint locale è stato caricato dal `serve_manager` e il server vLLM ha raggiunto `Application startup complete`. Il primo 404 era causato dal nome sbagliato inviato dal runner (`paddleocr-vl` invece di `PaddleOCR-VL-1.6`), quindi non era un problema del modello. Ripetuto con il nome esposto dal server, il test ha prodotto testo non vuoto. | Task | Esito | Wall | Output | |---|---:|---:|---| | text | superato, 10783 caratteri | 12.045 s | `data/benchmarks/bench_20260901T151055Z/outputs/paddleocr-vl/text-001.json` | Il primo tentativo resta comunque conservato nel report `data/benchmarks/bench_20260901T150511Z/report.json` come diagnostica di configurazione del target. Il runner salva anche un artefatto `*-error.json` per ogni nuova iterazione fallita. ### DeepSeek-OCR-2 — deploy e inferenza superati con offload Il 2026-09-01 il checkpoint locale è stato servito dal percorso UX con `--cpu-offload-gb 4`, necessario sulla RTX 4060 Laptop da 8 GB. Il server ha completato il warmup senza OOM e l’inferenza testuale ha prodotto output non vuoto. | Task | Esito | Wall | TTFT | Output | |---|---:|---:|---:|---| | text | superato, 380 caratteri | 17.760 s | 2.350 s | `data/benchmarks/bench_20260901T172157Z/outputs/deepseek-ocr/text-001.json` | La misura è di serving/inferenza, non ancora di qualità gold; il parser layout specifico DeepSeek resta da validare. Un tentativo layout successivo ha mostrato che il prompt layout DeepSeek può entrare in una generazione non delimitata: il server continua a produrre token senza chiudere lo stream e il test è stato interrotto manualmente dopo il timeout operativo. Non è una misura valida e non viene usata nel confronto; il log del server resta in `data/models/deepseek-ocr/.serve.log`. ## Confronto preliminare dei run validi Questa tabella confronta solo run con output conforme al task indicato. Non è una classifica di accuratezza: usa task diversi dove il protocollo del modello non è ancora omogeneo e non contiene metriche gold. | Adapter | Task | Output valido | Wall | TTFT | |---|---|---:|---:|---:| | MonkeyOCRv2-Parsing | text | 916 caratteri | 3.628 s | 0.758 s | | MinerU2.5 | text | 1116 caratteri | 3.337 s | 0.632 s | | PaddleOCR-VL | text | 10783 caratteri | 12.031 s | 0.566 s | | DeepSeek-OCR-2 | text | 380 caratteri | 17.774 s | 2.400 s | | Unlimited-OCR | end2end | 4 blocchi | 29.375 s | 1.929 s | ### GLM-OCR — download temporaneo riuscito, serving non sostenibile su 8 GB Il checkpoint completo è stato scaricato temporaneamente in `/tmp/tabularium-glm-ocr` (2.5 GB). La ricetta con speculative MTP fallisce perché il checkpoint non contiene i pesi MTP attesi da vLLM 0.28.0. Il fallback senza MTP ha poi caricato il modello base, ma la configurazione 16k/32k è andata in OOM; anche il profilo consumer 8k/8k non ha completato la profilazione FlashInfer. Non viene quindi dichiarato deploy riuscito. Un terzo tentativo con `gpu-memory-utilization=0.90` e contesto 8192 ha confermato il limite: il modello carica i pesi ma vLLM non riesce a inizializzare una cache KV sufficiente. GLM resta quindi non verificato su questa GPU, anche senza speculative decoding. Il 2026-09-02 è stato provato anche il profilo minimo ragionevole senza MTP, `max-model-len=4096`, `max-num-batched-tokens=8192` e quota GPU 0.90. I pesi sono stati caricati (2.21 GiB), ma l’engine è terminato durante il warmup con `Engine core initialization failed`; non è stata possibile alcuna inferenza. ### Dots OCR — download temporaneo riuscito, OOM in profilazione Il checkpoint completo è stato scaricato temporaneamente in `/tmp/tabularium-dots-ocr` (~5.7 GiB di pesi). Con offload CPU 4 GiB e contesto 8k i pesi sono stati caricati, ma vLLM ha terminato con `No available memory for the cache blocks` durante l’inizializzazione dell’encoder. Il modello resta non verificato sulla RTX 4060 Laptop da 8 GB. Un secondo tentativo con `gpu-memory-utilization=0.90` ha ridotto il margine solo fino a `-0.24 GiB` di cache disponibile e ha prodotto lo stesso errore; non è quindi sufficiente aumentare la quota GPU senza una quantizzazione o una GPU più capiente. Il 2026-09-02 il contesto è stato ridotto a 4096 con quota GPU 0.90. Il modello ha fallito esplicitamente con `torch.OutOfMemoryError` durante il forward vision (`7.45 GiB` già occupati su `7.62 GiB` visibili); anche dots.ocr non è quindi deployabile su questa scheda con la build vLLM corrente. È stata provata anche la combinazione più favorevole osservata (`--enforce-eager` e offload CPU 3 e poi 4 GiB). Il vision encoder ha comunque richiesto altri 338 MiB con soli 270 MiB liberi: l’errore è rimasto identico in entrambe le prove. Non viene forzata una riduzione artificiale della risoluzione, perché altererebbe il caso d’uso delle scansioni dense. È stato aggiunto un preflight al Registro: prima di avviare un download confronta spazio libero, dimensione stimata e un margine di 512 MiB. Con lo stato attuale il download persistente di GLM viene rifiutato subito con un messaggio esplicito, invece di saturare la partizione e fallire a metà. ## Runner benchmark Il runner interno è [scripts/benchmark_models.py](scripts/benchmark_models.py). Ogni esecuzione crea automaticamente `data/benchmarks/bench_/`, con un `report.json` e `outputs//` contenente il risultato grezzo completo di ogni iterazione. Questi file sono gli artefatti da conservare per confronti futuri; il report non sostituisce le annotazioni gold. Accetta più endpoint già avviati e registra, per ogni run: - esito e errore; - latenza totale e aggregati mediani; - TTFT, token/s e token usage restituiti dal server; - numero di elementi layout/end2end o caratteri riconosciuti; - validità OTSL per il task tabella. - configurazione raccomandata ed effettiva del modello, override salvati, workflow nativo e versione/argomenti principali della ricetta di serving. Il campo `output_file` nel report collega ogni misura al raw output esatto che l’ha prodotta. Ogni run conserva anche il proprio snapshot di configurazione nel report (`tabularium-vlm-benchmark-v3`), inclusi provider dichiarato ed endpoint redatti. Così due misure con override diversi non vengono scambiate per un confronto a parità di ricetta, e i token nelle URL non finiscono nei report. Il parametro `--timeout` è ora propagato a tutti i task (`layout`, `text`, `table` ed `end2end`), non soltanto a END2END: una generazione non delimitata non può più bloccare indefinitamente il benchmark. ### Collegamento Hugging Face da UX Per rispettare il vincolo «l'utente non tocca codice», il Registro modelli ora espone il flusso OAuth Device Code ufficiale di Hugging Face. L'amministratore preme **Collega Hugging Face**, autorizza l'app nel browser con il codice breve e la UI fa polling dello stato; il token resta nella cache della libreria HF e non viene esposto al frontend o ai log. Il download Qwen potrà essere ripreso dalla UI dopo l'autorizzazione. `ok` richiede inoltre un output conforme: una risposta HTTP con zero elementi layout/end2end, testo vuoto o OTSL non valido viene registrata come fallimento di protocollo, pur conservando il raw output per la diagnosi. ### Regressione runtime fresco — MinerU2.5 superata Il 2026-09-02 è stato ripetuto il serving da checkpoint locale con il runtime gestito dalla UX (`PATH` del venv, `CC=gcc-13`, `CXX=g++-13`, `NVCC_CCBIN=/usr/bin/g++-13`) e una cache FlashInfer non già pronta. Il server ha completato warmup e ha esposto `/v1/models`; il benchmark `text` ha prodotto 1116 caratteri validi in 3.337 s (TTFT 0.632 s, 198.24 token/s). Output: `data/benchmarks/bench_20260902T062359Z/outputs/mineru2.5/text-001.json`. Questo test ha anche isolato il fallback compiler appena aggiunto al `serve_manager`: senza gli override GCC il primo avvio falliva nella compilazione FlashInfer, mentre la configurazione UX completa passa. ### Benchmark omogeneo — PaddleOCR-VL superato Ripetuto il 2026-09-02 sullo stesso campione e con il task `text`, usando la ricetta vLLM del registry e il runtime UX completo. Il server ha completato il warmup e il nome esposto verificato è `PaddleOCR-VL-1.6`. L’output è valido: 10783 caratteri in 12.031 s (TTFT 0.566 s, 275.46 token/s; generazione terminata per il limite di 3072 token). Output: `data/benchmarks/bench_20260902T062607Z/outputs/paddleocr-vl/text-001.json`. ### Benchmark omogeneo — DeepSeek-OCR-2 superato Ripetuto il 2026-09-02 sullo stesso campione e con il task `text`, usando la ricetta vLLM del registry, 4 GiB di offload CPU e il runtime UX completo. Il server ha esposto correttamente `deepseek-ocr-2` e l’output è valido: 380 caratteri in 17.774 s (TTFT 2.400 s, 15.08 token/s). Output: `data/benchmarks/bench_20260902T063034Z/outputs/deepseek-ocr/text-001.json`. Esempio di confronto su due server già avviati: ```bash PYTHONPATH=backend data/vllm-runtime/bin/python scripts/benchmark_models.py \ --image test/1502-a-BANCO-SAN-GIORGIO-originale.jpg \ --target monkeyocrv2-parsing,http://127.0.0.1:8888/v1,MonkeyOCRv2 \ --target mineru2.5,http://127.0.0.1:8889/v1,mineru2.5 \ --task layout --repeat 2 --output data/benchmarks/run.json ``` Il terzo campo del target è opzionale: con `adapter_id,url` il runner usa automaticamente `capabilities.served_model_name`, cioè lo stesso nome con cui il Registro avvia il server. Questo evita mismatch fra id interno e nome vLLM. Per un adapter senza protocollo layout verificato usare `--task end2end`. Un confronto di qualità richiede inoltre il validation split gold del progetto; non è corretto confrontare soltanto il numero di blocchi o la velocità. ## Coda di verifica ### MinerU2.5: cap 1 MP del client violava il protocollo immagine — risolto (2026-09-02) Sintomo (istanza Vast RTX 5060 Ti, MinerU2.5 via tunnel): prefill two-stage produceva 155+ blocchi `Text` di ~32 px con contenuti da un solo carattere (`-`, `、`, `3`) su LSI_17186_015 — il layout del modello era un flood di micro-blocchi `page_number`, categoria sconosciuta ricaduta su `Text`. Causa (riprodotta live, non inferita): `_chat` applicava a **tutte** le immagini il tetto globale `VLLM_MAX_PIXELS=1.003.520` (pensato per MonkeyOCRv2/MOCR2). L'immagine di layout di MinerU2.5 è fissata a 1036x1036 = 1.073.296 pixel, quindi veniva ridimensionata a 1001x1001 prima dell'invio. A 1001x1001 il modello allucina la griglia di micro-blocchi (riprodotto con richiesta manuale); a 1036x1036 esatti produce il layout pulito (5 blocchi: 2 Title, 2 Text, 1 Table a piena pagina — identico all'output del client ufficiale `mineru-vl-utils` 1.2.1 eseguito sull'istanza stessa). Correzione: `layout()` non passa più la dimensione ufficiale per il cap; per i crop di testo/tabella gli adapter con `native_image_resolution` (MinerU2.5) sono esonerati dal tetto client — i crop partono a risoluzione nativa e il cap vero lo applica il preprocessore di vLLM dal `preprocessor_config` del checkpoint (max_pixels=1605632), come fa il client ufficiale. Verifica post-fix con il client Tabularium: 5 blocchi puliti, testo del crop titolo = `SHIPPING INDEX`. Nota: il difetto non era legato al cloud — colpiva qualsiasi serving di MinerU2.5, anche locale. Restano aperti solo test che richiedono un cambio di risorse o di hardware: 1. `qwen3-vl-8b`: almeno 16.3 GB liberi per il download persistente, oltre al margine operativo. Il 2026-09-02, con 26 GB liberi, il download via registry UX è stato avviato correttamente; HF anonimo ha trasferito 145.7 MB, poi si è fermato a `Fetching 16 files: 12/16`. Un resume nello stesso processo UX ha raggiunto 212.8 MB prima di restare senza ulteriore progresso osservabile. È stato cancellato lasciando 212.8 MB parziali riutilizzabili e circa 25 GB liberi. Nessun server o processo è rimasto attivo. Non è quindi una prova di serving né un fallimento della GPU; resta aperto il problema del download HF anonimo lento/bloccato. 2. `dots-ocr` e `glm-ocr`: GPU più capiente oppure checkpoint quantizzato ufficialmente supportato dal relativo progetto. 3. Benchmark di qualità: validation split gold comune e task omogeneo per confrontare davvero i modelli, oltre alle misure di latenza già raccolte. ## Risultato corrente Il percorso UI di build e il runtime CUDA sono verificati. Il catalogo contiene gli adapter e diversi checkpoint locali, ma il deploy/inferenza di tutti i modelli e il benchmark comparativo completo sono ancora **incompleti**. ### Nota sui dataset annotati I file `data/projects/5`, `6` e `9` presenti nel workspace sono dati di test, non un gold set validato dall’utente. Le prove generate in `data/benchmarks/gold_20260902T063434Z/` e `data/benchmarks/gold_20260902T063552Z/` sono quindi soltanto esplorative e non entrano in alcuna classifica o conclusione di qualità. Il runner `scripts/benchmark_gold.py` resta pronto per un futuro validation set realmente revisionato, ma non certifica il dataset che riceve. ### MinerU2.5 MLX nativo — pipeline ufficiale avviata e corpus verificato (2026-09-24) Il runtime macOS isolato usa ora `mineru-vl-utils[mlx]==2.0.5`: il pin precedente `1.0.5` caricava MLX-VLM 0.3.12 e Transformers 5.17, una combinazione che falliva durante il caricamento del processor (richiesta accidentale di PyTorch/Torchvision per il processor video Qwen2VL). La serie 2.0.5 installa l'extra MLX ufficiale `mlx-vlm>=0.7,<0.8`, compatibile con Transformers 5.14+; il gateway nativo `MinerUClient(backend="mlx-engine")` si avvia senza installare PyTorch. Verificato con le impostazioni raccomandate e senza override: | Pagina | Blocchi validi | Tabella | Caratteri | Wall | |---|---:|---:|---:|---:| | `LSI_17187_008` (registro) | 8/8 | 1 HTML | 8425 | 17,464 s | | `LSIVS_17186_004` (supplemento) | 80/80 | nessuna | 1236 | 15,072 s | Report completi e output grezzi: `data/benchmarks/mineru-mlx-20260924/`. Il protocollo è valido, ma la tabella del registro contiene righe ripetute; questo smoke test dimostra che il flusso ufficiale funziona, non che la trascrizione sia corretta. Serve confronto contro le annotazioni gold prima di valutare l'accuratezza o ottimizzare sampling e post-processing. ### Audit modelli su RTX Vast — copertura parziale (2026-09-24) I report `data/benchmarks/vast-rtx-pro-4000-20260924/` provano l'esecuzione del workflow nativo su GPU per quattro adapter: | Adapter | Pagine | Esito protocollo | Nota | |---|---:|---|---| | MonkeyOCRv2-Parsing | 3 | 3 valide | 7–34 blocchi per pagina | | dots.mocr | 3 | 3 valide | i report contengono anche una run fallita; le tre run finali sono valide | | PaddleOCR-VL 1.6 | 1 | valida dopo correzione | la prima run duplicava i blocchi (16); la run corretta ne produce 8 | | TeleOCR | 3 | 3 valide | workflow Detection; una pagina ha anche una prova Segmentation | Questi report sono schema v1: salvano output e tempi, ma non snapshot della configurazione effettiva. Dimostrano la validità del protocollo, non che ogni flag fosse identico alla ricetta raccomandata. Gli altri cinque adapter non hanno ancora un benchmark GPU Vast comparabile: MinerU2.5, GLM-OCR, DeepSeek-OCR-2, Unlimited-OCR e Qwen3-VL-8B. Qwen ha prove MLX locali, ma il workflow HTML nativo ha restituito zero bbox validi; non va conteggiato come test riuscito. I report schema v2 creati separatamente il 24 settembre registrano configurazione e override per Dots, Paddle e Qwen, ma non persistono provider/hardware e quindi non li attribuisco alla RTX Vast. Il runner corrente emette schema v3: registra provider esplicito e endpoint redatti (senza credenziali/query), oltre alla ricetta e agli override. Non deduce il provider dall'URL locale del tunnel; chi lancia il benchmark deve indicare `vast` nel target. Il report non interroga la GPU remota per dedurre modello o VRAM. La ricetta di provisioning cloud MinerU è stata aggiornata a `mineru-vl-utils[vllm]==2.0.5`, mantenendo vLLM 0.21.0. L'extra upstream 2.0.x ammette vLLM `>=0.19.1,<0.29.0`; il pin 0.21.0 rientra nell'intervallo e preserva la versione già usata sul checkpoint. Il test della ricetta verifica che il vecchio client 1.0.5 non venga più installato. Vedi [pyproject upstream](https://github.com/opendatalab/mineru-vl-utils/blob/main/pyproject.toml). Audit statico delle impostazioni: i controlli di serving sono applicati al comando vLLM o al costruttore del client ufficiale; generazione, immagini e workflow passano nei rispettivi adapter (inclusi i flag PaddleOCR-VL per il pipeline layout + riconoscimento). I test backend degli override e quelli della UI passano. Questo non sostituisce i benchmark GPU: in questa verifica il tunnel Vast risulta spento e il profilo attivo locale è PaddleOCR-VL su MLX. Perciò l'obiettivo di testare tutti e nove i modelli sulla RTX resta aperto. ### MonkeyOCRv2 — benchmark Vast su RTX 4060 Ti (2026-09-24) Il setup è stato completato sull'istanza Vast 4060 Ti scelta per il test, senza volumi preesistenti. Hardware rilevato: 16.380 MiB VRAM, compute capability 8.9, driver 595.84 (CUDA dichiarata 13.2), 19 GiB RAM e disco container da 50 GiB. Il server usa il runner ufficiale MonkeyOCRv2 al commit `6cc0c0bb4864f2541fa6bc3a8f976cffaac8f359`, vLLM 0.25.1, BF16, `--gpu-memory-utilization 0.90`, contesto 16.384 e DFlash ufficiale attivo. Caricati il checkpoint base (1,64 GiB) e il draft (0,16 GiB). Dopo il warmup vLLM ha riportato 5,31 GiB disponibili per KV cache; non è una misura di throughput concorrente. Workflow nativo `two_stage`, impostazioni raccomandate senza override. Tutte le pagine hanno rispettato il protocollo; questi conteggi non sono una misura di accuratezza gold. | Pagina | Blocchi validi | Caratteri | Wall | TTFT | Report | |---|---:|---:|---:|---:|---| | `LSI_17186_015` | 7/7 | 6.700 | 16,596 s | 0,619 s | `data/benchmarks/vast-rtx-4060-ti-20260924/monkeyocrv2/LSI_17186_015.json` | | `LSI_17187_008` | 7/7 | 6.481 | 7,231 s | 0,527 s | `data/benchmarks/vast-rtx-4060-ti-20260924/monkeyocrv2/LSI_17187_008.json` | | `LSIVS_17186_004` | 34/34 | 1.275 | 21,239 s | 0,474 s | `data/benchmarks/vast-rtx-4060-ti-20260924/monkeyocrv2/LSIVS_17186_004.json` | Questi report schema v3 registrano provider esplicito, ricetta, workflow e override effettivi. Il primo errore di benchmark (`Operation not permitted`) era il sandbox locale che bloccava il collegamento al tunnel; la prova è stata ripetuta con rete locale autorizzata e il risultato riportato sopra è valido. Il server è rimasto attivo sull'istanza al termine delle tre prove. ### MinerU2.5 — benchmark Vast con client ufficiale (2026-09-24) Sulla stessa RTX 4060 Ti, la ricetta ha installato vLLM 0.21.0 e `mineru-vl-utils[vllm]==2.0.5`; il percorso verificato è `MinerUClient(backend="http-client")` con il suo pipeline layout + OCR, non una richiesta diretta al solo modello. Il primo avvio ha trovato due difetti di integrazione: il bridge partiva prima che vLLM fosse pronto, e veniva passato al client il repo HF invece del nome servito (`mineru2.5`). Lo script ora attende che `/v1/models` esponga il nome atteso, poi avvia MinerUClient con lo stesso id. La readiness di entrambi gli endpoint e le tre prove sotto sono passate. | Pagina | Blocchi validi | Caratteri | Wall | Report | |---|---:|---:|---:|---| | `LSI_17186_015` | 8/8 | 8.560 | 18,620 s | `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5/LSI_17186_015.json` | | `LSI_17187_008` | 8/8 | 8.425 | 17,099 s | `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5/LSI_17187_008.json` | | `LSIVS_17186_004` | 80/80 | 1.236 | 19,810 s | `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5/LSIVS_17186_004.json` | Esito 3/3 valido a livello di protocollo; il numero di blocchi e i caratteri non certificano accuratezza senza annotazioni gold. I report schema v3 registrano endpoint nativo, recipe e impostazioni effettive. #### MinerU2.5 — riprovisioning sulla stessa RTX e regressione dei log Il 24 settembre il controllo in sola lettura dell’account ha ritrovato la RTX 4060 Ti attiva con Dots in servizio. Il checkpoint MinerU e l’ambiente `mineru-vl-utils 2.0.5` / vLLM 0.21.0 erano già presenti; il setup ha quindi riusato entrambi, superato il preflight con 15 GB liberi di VRAM e 13 GB su disco, e sostituito solo il server Tabularium sulla porta 8888. Il primo stato UI appariva fallito perché il parser attribuiva al nuovo avvio l’`EngineDeadError` registrato durante lo shutdown intenzionale di Dots. Corretti il filtro del log e aggiunto un test di regressione. | Pagina | Blocchi validi | Caratteri | Wall | Report | |---|---:|---:|---:|---| | `LSI_17186_015` | 8/8 | 8.560 | 17,828 s | `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5-reprovisioned/LSI_17186_015.json` | | `LSI_17187_008` | 8/8 | 8.425 | 15,455 s | `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5-reprovisioned/LSI_17187_008.json` | | `LSIVS_17186_004` | 80/80 | 1.236 | 12,661 s | `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5-reprovisioned/LSIVS_17186_004.json` | I report schema v3 confermano provider Vast, endpoint nativo, ricetta e impostazioni effettive senza override. Il risultato resta una verifica del protocollo, non dell’accuratezza rispetto al gold. ### PaddleOCR-VL 1.6 — benchmark Vast con workflow nativo (2026-09-24) Sulla stessa RTX 4060 Ti, PaddleOCR-VL è stato servito con vLLM 0.28.0 e la ricetta del catalogo: `--trust-remote-code`, `--max-num-batched-tokens 16384`, cache dei prefissi disattivata e cache multimodale del processor a zero. Il server ha esposto `PaddleOCR-VL-1.6`; il benchmark ha chiamato l'adapter nativo `paddleocr-vl`, che usa il suo workflow ufficiale layout + riconoscimento. | Pagina | Blocchi validi | Caratteri | Wall | Report | |---|---:|---:|---:|---| | `LSI_17186_015` | 8/8 | 8.331 | 35,236 s | `data/benchmarks/vast-rtx-4060-ti-20260924/paddleocr-vl/LSI_17186_015.json` | | `LSI_17187_008` | 8/8 | 8.332 | 16,419 s | `data/benchmarks/vast-rtx-4060-ti-20260924/paddleocr-vl/LSI_17187_008.json` | | `LSIVS_17186_004` | 35/35 | 1.277 | 8,976 s | `data/benchmarks/vast-rtx-4060-ti-20260924/paddleocr-vl/LSIVS_17186_004.json` | Esito 3/3 valido a livello di protocollo; non è un punteggio di accuratezza gold. I report schema v3 riportano provider Vast, endpoint redatto, ricetta e impostazioni effettive. Durante il serving il modello ha occupato quasi tutta la VRAM disponibile (887 MiB liberi osservati dopo il warmup), quindi questa configurazione è verificata su 16 GB ma lascia poco margine per altri processi GPU concorrenti. ### dots.mocr — benchmark Vast con chiamata upstream (2026-09-24) Il checkpoint da 5,7 GiB è stato servito sulla RTX 4060 Ti con vLLM 0.28.0, BF16, `--gpu-memory-utilization 0.9`, `--trust-remote-code` e `--chat-template-content-format string`. vLLM ha usato il contesto del checkpoint (131.072 token), FlashAttention 2 e 4,35 GiB di KV cache. La prova usa la chiamata del parser upstream: prompt `prompt_layout_all_en`, temperatura 0.1, `top_p=1.0`, `max_completion_tokens=32768`, massimo immagine 11.289.600 pixel, token immagine `<|img|><|imgpad|><|endofimg|>` e risposta OpenAI non-streaming. | Pagina | Blocchi validi | Caratteri | Wall | Report | |---|---:|---:|---:|---| | `LSI_17186_015` | 8/8 | 8.522 | 60,455 s | `data/benchmarks/vast-rtx-4060-ti-20260924/dots-ocr/LSI_17186_015-official.json` | | `LSI_17187_008` | 8/8 | 8.303 | 65,635 s | `data/benchmarks/vast-rtx-4060-ti-20260924/dots-ocr/LSI_17187_008-official.json` | | `LSIVS_17186_004` | 35/35 | 1.281 | 31,647 s | `data/benchmarks/vast-rtx-4060-ti-20260924/dots-ocr/LSIVS_17186_004-official.json` | Esito 3/3 valido a livello di protocollo, non di accuratezza gold. Non-streaming non espone TTFT o token/s: entrambi restano `null`. Un primo tentativo sul vecchio tunnel SSH si è interrotto; con un tunnel keepalive il protocollo upstream ha completato le tre richieste. La VRAM libera dopo il warmup è circa 430 MiB: il modello è utilizzabile sulla 4060 Ti, senza margine per carichi GPU concorrenti. Fonte primaria: [repository ufficiale dots.mocr](https://github.com/studio-dots-ai/dots.mocr), sezioni “vLLM inference” e “Document Parse”. ### Stato risorse dei modelli ancora da provare su questa RTX — 2026-09-24 Prima di scaricare Dots, il filesystem overlay da 50 GB aveva 11 GB liberi e 7,6 GB erano cache pip rigenerabile degli ambienti già installati. È stata svuotata solo quella cache; checkpoint ed ambienti verificati sono stati preservati. Dopo aver scaricato Dots e aver completato i test, il controllo remoto mostra circa 13 GB liberi. GLM-OCR (23 GB), TeleOCR (23 GB) e DeepSeek-OCR-2 (27 GB) richiedono più spazio per installare i rispettivi ambienti con pin incompatibili; i valori sono i preflight `resource_budget()` e non verranno aggirati. Unlimited-OCR richiede la propria immagine Docker (budget iniziale 19 GB), che non è quella di questa istanza. Qwen3-VL-8B richiede 23 GB di VRAM secondo il budget della ricetta, oltre ai 16 GB totali della RTX 4060 Ti. Sono limiti di provisioning/hardware, non esiti negativi d’inferenza: completare la matrice su questa istanza richiede più disco e, per Qwen, una GPU con più VRAM o una ricetta quantizzata ufficialmente supportata. ### MinerU2.5 — controllo live della tabella annotata (2026-09-24) Verificato nuovamente il server già in esecuzione sulla stessa istanza, senza sostituirlo: RTX 4060 Ti, 13 GB liberi su disco, 1.489 MiB liberi di VRAM, vLLM 0.21.0 e gateway `MinerUClient` ufficiale. Il benchmark nativo sulla pagina `LSI_17186_015` ha completato il workflow end-to-end in 24,724 s: 8/8 elementi hanno bbox valido e il risultato contiene una tabella HTML. La tabella prodotta (`[140.6, 600.7, 2723.7, 3735.8]` px) ha IoU **0,873** con il solo riquadro `Table` confermato disponibile per questa pagina (`[380, 540, 2750, 3700]`). È una misura di una regione su una pagina, non una valutazione globale di layout o trascrizione. Lo snapshot conferma che il test ha usato il workflow `official`, senza override, e i default dichiarati per MinerU (`layout_image_size 1036×1036`, `min_image_edge 28`, `max_image_edge_ratio 50`). Il report riproducibile, inclusi output e ricetta, è `data/benchmarks/vast-rtx-4060-ti-20260924/mineru2.5-live-goldcheck/report.json` (file ignorato dal repository insieme agli altri report di benchmark). L’istanza è sana e utilizzabile, ma lo spazio libero resta sotto i preflight di GLM-OCR, TeleOCR e DeepSeek-OCR-2; Qwen3-VL-8B resta oltre la VRAM totale. Questa verifica quindi non rimuove i blocker della matrice restante. ### Unlimited-OCR su MLX — percorso locale Apple Silicon (2026-10-13) `mlx-vlm` 0.6.4 ha aggiunto il modello `unlimited_ocr` con `RingSlidingKVCache` (la R-SWA di upstream: prefisso intero + anello degli ultimi 128 token). È il pezzo che prima mancava e che rendeva il modello «solo remoto»: fino a 0.6.3 la pagina produceva `alpha.alpha.alpha…` fino al tetto dei token. Misurato su **M5 Pro / 24 GB** (la macchina di riferimento della card), non da shell ma con l’`mlx_vlm 0.7.1` che Tabularium installa già in `/mlx-runtime`, sulla pagina indice `LSI_4322_045` (2816×3680): | Config (3000 token max) | tok/s | TTFT | Esito | |---|---:|---:|---| | `mxfp8`, gundam, nessun guard | 265,6 | 1,2 s | loop: una riga ripetuta 23× | | `mxfp8`, gundam, `rep 1.1` (finestra 20) | 248,4 | 1,3 s | loop: 80 ripetizioni, la finestra non copre la riga | | `mxfp8`, gundam, `rep 1.1` + **finestra 128** | 251,1 | 0,9 s | **nessuna ripetizione, `finish=stop`** a 2917 token | | `mxfp8`, base, `rep 1.1` | 122,1 | 1,1 s | ~2× più lento di gundam, loop con finestra 20 | Output valido: grounded-markdown con `<|det|>header [122,42,262,62]<|/det|>` e una `` HTML completa con le righe reali della pagina indice del campione — lo stesso formato che `parse_layout` già legge. Riferimento «prima»: sullo stesso tipo di pagina il percorso vLLM su RTX 4060 Laptop 8 GB in offload dava 13,3 tok/s; qui non c’è cloud e si sta ~20× più veloci. **Best practice dedotte** (dalla doc `mlx-vlm/models/unlimited_ocr/README.md` e misurate): 1. Il prompt è il testo puro (`document parsing.`): `` lo inserisce il template, non va aggiunto a mano. 2. `gundam` (default: `cropping=True`, `image_size=640`, `base_size=1024`) è il modo per la pagina singola ed è ~2× più veloce di `base`; `base` è documentato per il multi-pagina, che Tabularium non usa (riconosce pagina per pagina). 3. Il **no-repeat n-gram processor è opt-in e `mlx-vlm` non lo fornisce**, e il server MLX non accetta processor callable via JSON. Il sostituto è `repetition_penalty`; la **finestra conta**: con il default (20 token) una riga di registro (~25 token) scivola fuori e non viene mai penalizzata. `repetition_context_size=128` è il valore che chiude il loop. 4. `temperature=0.0` per output deterministico; `max_tokens` alto (il modello è nato per documenti lunghi). I default esposti da Tabularium (`unlimited-ocr`, sezione MLX) sono questi: variante `bf16` (misura pulita) o `8bit` (circa metà dei pesi), `repetition_penalty` 1.1 e `repetition_context_size` 128, applicati **solo** al serving locale Apple Silicon (su vLLM il modello ha già il suo processore n-gram). Resta da fare la prova di **qualità** contro annotazioni gold: questa è una verifica di protocollo, non un punteggio. #### Confronto varianti MLX (gundam, rep 1.1 + finestra 128, 3000 token) | Variante | tok/s | wall | picco | righe | ripetizioni | Note | |---|---:|---:|---:|---:|---:|---| | `Unlimited-OCR-8bit` (3,6 GB) | **239,6** | 14,3 s | **5,9 GB** | 67 | 1 | più veloce e più leggera | | `Unlimited-OCR-bf16` (6,2 GB) | 154,4 | 22,9 s | 8,7 GB | 68 | 1 | fedeltà massima, la più lenta | | `Unlimited-OCR-mxfp8` (3,6 GB) | 224,7 | 17,2 s | 8,7 GB | 73 | 1 | **artefatti di detokenizzazione** (`Ġ` al posto degli spazi) | L'8-bit è ~1,5× più veloce del bf16 a parità di contenuto riconosciuto, e su un Mac da 16 GB è la variante che entra. Il bf16 resta il default perché è la misura pulita: la differenza di qualità si può affermare solo contro il gold. Un avvertimento su `mxfp8` (non esposto): decodifica con gli spazi resi come `Ġ`, quindi le trascrizioni escono sporche. #### Il server MLX standard non basta: server dedicato `mlx_vlm.server` 0.7.1 **non inietta l'immagine** per l'architettura `unlimited_ocr`: costruisce il prompt coi soli token di testo (`prompt_tokens` 6-7 contro i 1518 dell'API Python) e il modello restituisce vuoto o testo allucinato. Il difetto è nel backend a continuous batching, non nel formato della richiesta: `prepare_inputs` espande correttamente a 1518 token sia col prompt testuale sia con la lista di messaggi. Tabularium quindi serve Unlimited-OCR con un server dedicato (`backend/app/services/unlimited_ocr_mlx_server.py`), lanciato nel venv MLX: espone la stessa interfaccia OpenAI ma si regge sull'API `mlx_vlm.generate` che funziona. Verificato end-to-end (streaming e non): `prompt_tokens=1518`, col guard `finish=stop` e zero ripetizioni; senza guard il loop. Poiché espone la stessa interfaccia, il resto del client resta invariato.