Fluttorch
Porta un modello PyTorch in un’app Flutter e ferma la build quando l’export o la quantizzazione spostano i numeri.
fluttorch: ^1.0.0// 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'),);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.
atol 0.1rtol 0.1cos ≥ 0.998La 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.
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.