Fluttorch

Porta un modello PyTorch in un’app Flutter e ferma la build quando l’export o la quantizzazione spostano i numeri.

pubspec.yamlfluttorch: ^1.0.0
fluttorch.dart
// generata dal manifest scritto dall’esportatore, mai a manofinal forecast = await SolarForecast.load(runtime, artifact: bytes);final out = await forecast.run(features: window);// nei test, con la tolleranza che arriva dal manifestawait expectParity( forecast.model, goldens: await DirectoryGoldenBundle.open('build/solar_forecast.fluttorch.json'),);
Il prodotto

Ponte tipizzato fra l’export di PyTorch e un’app Flutter. L’esportatore scrive artefatto, manifest e golden in un comando solo, e l’API Dart si genera dal manifest. I golden si rigiocano nei test: il gate di parità confronta le uscite con la tolleranza registrata e ferma la build quando la supera.

solar_forecast.fluttorch.jsonIn attesa
load_mw float32[4]xnnpack · int8-static
#golden dell’exportdispositivo|Δ|esito
[0]·····················
[1]·····················
[2]·····················
[3]·····················
tolleranzaatol 0.1rtol 0.1cos ≥ 0.998
in attesa

La build si ferma qui. Lo scarto è 1.72 su un limite di 0.1, e il report nomina il tensore, il backend e la quantizzazione con cui è stato misurato.

cosa succede quando i numeri cambiano

Mettere un modello su un telefono vuol dire esportarlo, e quasi sempre quantizzarlo. Tutti e due i passaggi cambiano i numeri, e niente nella catena di strumenti dice quando cambiano troppo: la build passa, l’app parte, l’uscita sembra plausibile e il modello è peggiore di quello valutato prima di esportarlo. Il preprocessing sbaglia dal lato opposto. Scritto una volta in Python per l’addestramento e una seconda in Dart per il servizio, le due copie divergono al primo refactor. Fluttorch chiude le due falle con un documento solo. L’esportatore scrive un manifest accanto all’artefatto, l’API Dart viene generata da lì e i golden catturati durante l’export si rigiocano nella suite di test. Forme, dtype, preprocessing, etichette e hash dei pesi li dichiara il manifest e li leggono tre consumatori: il generatore di codice, il gate di parità e il runtime. Dal lato Dart nessuno di quei valori viene riscritto a mano, quindi nessuno può contraddire l’addestramento. Il gate è il motivo per cui il progetto esiste. I binding tipizzati sono una comodità; intercettare la deriva numerica è la parte che non fa nessun altro. expectParity rigioca i golden e misura lo scarto. Se il limite salta la build si ferma, e il report dice quale tensore lo ha superato e su quale backend è stato misurato. Dove l’export ha catturato i tap, indica anche il primo layer i cui numeri si sono mossi. E il runtime rifiuta un artefatto il cui hash non è quello che il suo manifest ha registrato: è il controllo che prende la ri-esportazione fatta a metà. ExecuTorch è il primo backend, non l’architettura. Un gate di parità vale allo stesso modo su LiteRT e ONNX Runtime, e l’interfaccia di runtime è l’unica cucitura che un backend tocca: sopra quella linea nessuno sa quale motore sta eseguendo il modello. Le capacità sono riportate e non date per scontate, perché esporre le attivazioni intermedie o eseguire in modo deterministico dipende dall’hardware e non dalla build: ogni report dice quale backend l’ha prodotto. Alla 1.0.0 l’API è congelata e la politica di deprecazione è scritta: un’API deprecata sopravvive ad almeno due minor, che è la politica di ExecuTorch e non un numero scelto qui. Le tolleranze sono misurate su due modelli invece che scelte, e la misura ha detto una cosa che non ci si aspetta: il modello più semplice sbanda fino a otto volte di più, perché le sue uscite sono grandi e l’errore relativo si misura sull’uscita mentre l’arrotondamento avviene sugli intermedi. Quello che la 1.0 non promette è che ogni motore esegua ogni modello: ExecuTorch lowera VoltaCast e poi fallisce a eseguirlo, identico sotto il suo runtime Python, e la suite lo registra come aspettativa fallita perché il giorno che a monte lo sistemano, lo dica. Le tre binding native restano fuori da pub.dev, che non porta la metà nativa.

Ambito
Open sourceDartFlutterPyTorchOn-device ML
Approccio

Cosa abbiamo fatto

01Schema del manifest, con scrittore Python e lettore Dart che producono lo stesso documento
02Esportatore fluttorch-export: artefatto, manifest e golden in un comando
03Generatore build_runner dal manifest all’API Dart tipizzata
04Gate di parità: rigioco dei golden, metriche di deriva e report che ferma la build
05Attribuzione della deriva al primo layer che si muove, dove il backend espone i tap
Punti di forza
+Il manifest è l’unica fonte: forme, dtype, preprocessing, etichette e hash dei pesi
+API Dart generata dal manifest: passare il tensore sbagliato è un errore di compilazione
+Il gate ferma la build e nomina il tensore, il limite e il backend che l’ha misurato
+Il runtime rifiuta un artefatto il cui hash non è quello registrato nel manifest
+Le capacità del dispositivo le dichiara il backend: la build non le dà per scontate
+ExecuTorch è il primo backend: LiteRT e ONNX Runtime stanno dietro la stessa cucitura
Risultati ottenuti
1.0.0: API congelata, con politica di deprecazione scritta in STABILITY.md
Tolleranze misurate su due modelli invece che scelte, con lo strumento che le riproduce
Tre motori dietro la stessa interfaccia: ExecuTorch, LiteRT e ONNX Runtime
VoltaCast, Transformer seq2seq sulla domanda elettrica italiana, tiene i suoi golden a 5.96e-7 su LiteRT
Scrittore Python e lettore Dart producono lo stesso manifest byte per byte, denormali e zero negativo compresi
Tre pacchetti su pub.dev: fluttorch, fluttorch_gen e fluttorch_test
Risorse
Apri su GitHubVisita il sitoRepository

Ti piace quello che facciamo?

Parliamo del tuo prossimo progetto insieme