benchmarks.md
Rama main · réplica de lectura del repositorio original.
# 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_<UTC>/`, con un
`report.json` e `outputs/<adapter_id>/` 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
`<root>/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 `<table>` 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.`): `<image>` 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.