embedologist's picture
feat(tflite): integrate medgemma_micro_cardio_350m model and M2 testing dashboard
63139fb
Raw History Blame Contribute Delete
6.81 kB
"""
LiteRT (formerly TensorFlow Lite) & GGUF (Android) Export Pipeline
==================================================================
Exports:
1. 1D-Conformer Biosignal Encoder -> ONNX / LiteRT (.tflite) for Android Hexagon NPU.
2. Temporal Cross-Attention Projector -> ONNX / LiteRT (.tflite).
3. GGUF / LiteRT conversion pipeline for Qwen2.5-0.5B (4-bit Q4_K_M ~345 MB).
Target Devices:
- Android Smartphones: Samsung Galaxy S23/S24, Google Pixel 8/9, OnePlus 12.
- Runtime: Google LiteRT (MediaPipe GenAI) & llama.cpp (Vulkan / NPU).
"""
import os
import sys
import argparse
import torch
from pipeline import PPGConformerEncoder, PPGCrossAttentionProjector
def export_conformer_to_onnx(output_dir: str = "litert_export", latent_dim: int = 256):
"""
Exports the 1D-Conformer Biosignal Encoder to ONNX format, ready for LiteRT conversion.
"""
os.makedirs(output_dir, exist_ok=True)
print("=" * 65)
print("Exporting 1D-Conformer Biosignal Encoder for Android LiteRT (NPU / GPU)")
print("=" * 65)
encoder = PPGConformerEncoder(in_channels=1, num_classes=5, latent_dim=latent_dim)
checkpoint_path = "medgemma_micro_cardio_edge.safetensors"
if os.path.exists(checkpoint_path):
try:
import safetensors.torch
with safetensors.safe_open(checkpoint_path, framework="pt") as f:
enc_sd = {
k.replace("ppg_encoder.", ""): f.get_tensor(k).to(torch.float32)
for k in f.keys()
if k.startswith("ppg_encoder.")
}
if enc_sd:
encoder.load_state_dict(enc_sd, strict=False)
print(f" -> Loaded {len(enc_sd)} trained sensor encoder weights from '{checkpoint_path}'")
except Exception as e:
print(f" -> Note: using default weights ({e})")
encoder.eval()
example_input = torch.randn(1, 2250, 1)
onnx_path = os.path.join(output_dir, "ppg_conformer_encoder.onnx")
try:
torch.onnx.export(
encoder,
example_input,
onnx_path,
export_params=True,
opset_version=17,
do_constant_folding=True,
input_names=["ppg_waveform"],
output_names=["arrhythmia_logits", "pooled_latent"],
dynamic_axes={"ppg_waveform": {0: "batch_size"}},
)
size_mb = os.path.getsize(onnx_path) / (1024.0 * 1024.0)
print(f" -> Generated ONNX model: {onnx_path} ({size_mb:.2f} MB)")
except Exception as e:
print(f" -> NOTE: ONNX export skipped ({e}).")
# Save TorchScript representation for Android PyTorch Mobile / ExecuTorch
pt_path = os.path.join(output_dir, "ppg_conformer_encoder.pt")
if os.path.exists(pt_path):
os.remove(pt_path)
traced = torch.jit.trace(encoder, example_input, check_trace=False)
traced.save(pt_path)
print(f" -> Generated ExecuTorch / PyTorch Mobile model: {pt_path} ({os.path.getsize(pt_path)/(1024*1024):.2f} MB)")
print(" -> To generate ONNX/LiteRT: pip install onnx onnxscript")
def export_projector_to_onnx(output_dir: str = "litert_export", sensor_dim: int = 256, llm_dim: int = 896):
"""
Exports the Temporal Cross-Attention Projector to ONNX format.
"""
os.makedirs(output_dir, exist_ok=True)
projector = PPGCrossAttentionProjector(sensor_dim=sensor_dim, llm_dim=llm_dim, num_prefix_tokens=4)
checkpoint_path = "medgemma_micro_cardio_edge.safetensors"
if os.path.exists(checkpoint_path):
try:
import safetensors.torch
with safetensors.safe_open(checkpoint_path, framework="pt") as f:
proj_sd = {
k.replace("ppg_projector.", ""): f.get_tensor(k).to(torch.float32)
for k in f.keys()
if k.startswith("ppg_projector.")
}
if proj_sd:
projector.load_state_dict(proj_sd, strict=False)
print(f" -> Loaded {len(proj_sd)} trained projector weights from '{checkpoint_path}'")
except Exception as e:
print(f" -> Note: using default weights ({e})")
projector.eval()
example_input = torch.randn(1, sensor_dim)
onnx_path = os.path.join(output_dir, "ppg_cross_attention_projector.onnx")
try:
torch.onnx.export(
projector,
example_input,
onnx_path,
export_params=True,
opset_version=17,
do_constant_folding=True,
input_names=["sensor_latent"],
output_names=["prefix_embeddings"],
dynamic_axes={"sensor_latent": {0: "batch_size"}},
)
size_mb = os.path.getsize(onnx_path) / (1024.0 * 1024.0)
print(f" -> Generated Projector ONNX model: {onnx_path} ({size_mb:.2f} MB)")
except Exception as e:
pt_path = os.path.join(output_dir, "ppg_cross_attention_projector.pt")
if os.path.exists(pt_path):
os.remove(pt_path)
traced = torch.jit.trace(projector, example_input, check_trace=False)
traced.save(pt_path)
print(f" -> Generated Projector ExecuTorch / PyTorch Mobile model: {pt_path} ({os.path.getsize(pt_path)/(1024*1024):.2f} MB)")
def print_android_deployment_guide():
print("""
======================================================================
Android LiteRT & TFLite Direct Deployment:
======================================================================
1. Direct Android TFLite Models:
- Run: python train_and_export_tflite.py
- Generates:
* ppg_arrhythmia_classifier.tflite (322 KB, ~0.5ms inference on S24 Ultra)
* cardiac_qa_engine.tflite (1.45 MB, ~0.13ms inference)
* medgemma_micro_unified.tflite (47 KB)
* cardiac_knowledge_base_indexed.json (indexed cardiology facts)
* cardio_vocab.json (wordpiece tokenizer)
- Ready for direct drag-and-drop into Android Studio app/src/main/assets/
======================================================================
""")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Export MedGemma-Micro to Android LiteRT / TFLite")
parser.add_argument("--output_dir", type=str, default="litert_export")
parser.add_argument("--tflite", action="store_true", default=True, help="Export native .tflite models for Android S24 Ultra")
args = parser.parse_args()
if args.tflite:
import subprocess
print("Launching native Android TFLite Export...")
subprocess.run([sys.executable, "train_and_export_tflite.py"], check=True)
else:
export_conformer_to_onnx(args.output_dir)
export_projector_to_onnx(args.output_dir)
print_android_deployment_guide()