15. Model Updates OTA
Chapter 15 of 18 · 20 min
Over-the-air (OTA) model updates deliver improved ML models to deployed devices without physical access. Production OTA pipelines must handle version management, rollback capability, encryption, and bandwidth efficiency.
Version manifest schema:
import json
import hashlib
def create_model_manifest(model_path, metadata=None):
"""Create version manifest for OTA deployment"""
with open(model_path, 'rb') as f:
content = f.read()
checksum = hashlib.sha256(content).hexdigest()
size_bytes = len(content)
manifest = {
"version": metadata.get("version", "1.0.0"),
"checksum_sha256": checksum,
"size_bytes": size_bytes,
"model_type": metadata.get("type", "classifier"),
"min_compatible_version": metadata.get("min_version", "0.9.0"),
"release_date": metadata.get("date"),
"accuracy_metrics": metadata.get("metrics", {}),
"deprecation_date": metadata.get("deprecates_after"),
}
return manifest
def sign_manifest(manifest, private_key_path):
"""Sign manifest with Ed25519 for authenticity"""
from cryptography.hazmat.primitives import ed25519
with open(private_key_path, 'rb') as key_file:
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(key_file.read())
message = json.dumps(manifest, sort_keys=True).encode()
signature = private_key.sign(message)
signed_manifest = {**manifest, "signature": signature.hex()}
return signed_manifest
Delta updates for bandwidth efficiency:
import bsdiff4
def compute_delta(old_model_path, new_model_path, delta_path):
"""Compute binary delta between model versions"""
with open(old_model_path, 'rb') as old:
with open(new_model_path, 'rb') as new:
with open(delta_path, 'wb') as delta:
delta.write(bsdiff4.diff(old.read(), new.read()))
# Report savings
new_size = os.path.getsize(new_model_path)
delta_size = os.path.getsize(delta_path)
print(f"Full model: {new_size/1024:.1f}KB, Delta model: {delta_size/1024:.1f}KB")
def apply_delta(model_path, delta_path, output_path):
"""Apply delta to produce new model"""
with open(model_path, 'rb') as old:
with open(delta_path, 'rb') as delta:
with open(output_path, 'wb') as output:
output.write(bsdiff4.patch(old.read(), delta.read()))
iOS model update via App Group:
import Foundation
import BackgroundTasks
class ModelUpdateManager {
static let modelUpdateTask = "com.app.modelupdate"
func registerBackgroundTask() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.modelUpdateTask, using: nil) { task in
self.handleModelUpdate(task: task as! BGProcessingTask)
}
}
func scheduleModelUpdate() {
let request = BGProcessingTaskRequest(identifier: Self.modelUpdateTask)
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
try? BGTaskScheduler.shared.submit(request)
}
private func handleModelUpdate(task: BGProcessingTask) {
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
Task {
do {
let newModel = try await downloadModel()
try installModel(newModel)
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
}
private func installModel(_ model: URL) throws {
let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.app"
)!
let destURL = containerURL.appendingPathComponent("model.mlmodel")
try? FileManager.default.removeItem(at: destURL)
try FileManager.default.moveItem(at: model, to: destURL)
}
}
Android Play ML Model delivery:
// Use Play ML Model Delivery API
val modelDownloadConditions = Conditions.Builder()
.require(Condition.INTERNET)
.require(Condition.DEVICE_CHARGING)
.require(Condition.NOT_LOW_BATTERY)
.build()
val downloadTask = ModelManager.getInstance()
.update(Constants.MODEL_NAME)
.addConditions(modelDownloadConditions)
.addOnFailureListener { e ->
Timber.e(e, "Model download failed")
}
.addOnSuccessListener { model ->
// Model downloaded successfully
updateActiveClassifier(model)
}
// Continue app operation while model downloads
downloadTask.addOnProgressListener { status ->
progressBar.progress = status
}
EXERCISE
Implement a model versioning system with delta updates, create signed manifests, and test rollback from version 2.0.0 to version 1.5.0 using binary diffs.