
On-Device ML Crash Debugging: Core ML, ML Kit & TFLite
On-device machine learning has become a cornerstone of modern mobile apps, powering everything from real-time object detection and text recognition to pose estimation and smart replies. Frameworks like Apple's Core ML, Google's ML Kit, and TensorFlow Lite (TFLite) make it possible to run sophisticated models directly on a smartphone without round-tripping to the cloud. But when on-device ML inference crashes, the resulting stack traces are often cryptic, spanning native C++ layers, GPU delegate internals, and model interpreter abstractions that most mobile developers never touch directly. Debugging on-device ML crashes requires understanding the unique failure modes of each framework, the hardware-specific constraints of neural engine and GPU acceleration, and the threading hazards that arise when inference runs on real-time camera frames.
If you are already using Bugspulse to monitor your app's stability, you have likely seen crash clusters that trace back to MLModel.load(contentsOf:), Interpreter.run(), or FaceDetector.process(). This guide covers the most common on-device ML crash patterns across Core ML, ML Kit, and TensorFlow Lite, with concrete fixes and observability strategies for each platform.
Core ML Crash Patterns and Fixes
Core ML is Apple's framework for running machine learning models on iOS, iPadOS, macOS, watchOS, and visionOS. It leverages the Apple Neural Engine (ANE), GPU, and CPU depending on the model configuration and device capabilities. Most Core ML crashes in production fall into three buckets: model compilation failures, runtime inference errors, and multi-model resource exhaustion.
Model Compilation and Loading Failures
The first point of failure is model loading itself. Core ML compiles .mlmodel or .mlpackage files into a runtime-optimized format on first use. When compilation fails, you see errors like "Unable to load model" or "Failed to compile model" without much detail.
// Common crash pattern — no validation before loading
guard let modelURL = Bundle.main.url(forResource: "ObjectDetector", withExtension: "mlmodelc") else {
// Model not found or not compiled — crashes downstream
return
}
let model = try MLModel(contentsOf: modelURL) // Can throw if model is incompatibleCore ML models compiled on a newer macOS or Xcode version may not be backward-compatible. Always validate model loading and handle errors on a background queue — large models (100MB+) can trigger watchdog termination. See our iOS Watchdog Termination guide for details.
func loadModelSafely(named name: String) -> MLModel? {
guard let modelURL = Bundle.main.url(forResource: name, withExtension: "mlmodelc") else {
return nil
}
do {
return try MLModel(contentsOf: modelURL)
} catch {
print("Model compilation failed: \(error.localizedDescription)")
return nil
}
}Input Tensor Shape Mismatches
Core ML crashes silently or throws NSException when input tensor shapes don't match what the model expects. This is especially common with vision models where input images must be resized to a fixed dimension (224x224, 299x299, etc.) and converted to the correct pixel format.
// The model expects 224x224 RGB input but you pass a 1920x1080 BGRA image
let input = try MyModelInput(imageAt: originalImageURL)
let output = try model.prediction(input: input) // Crash: shape mismatchAlways check input constraints before running inference. Core ML exposes model.modelDescription.inputDescriptionsByName which contains shape and type metadata.
func validateInput(image: CVPixelBuffer, for model: MLModel) -> Bool {
guard let inputDesc = model.modelDescription.inputDescriptionsByName["image"],
let imageConstraint = inputDesc.imageConstraint else {
return false
}
let expectedWidth = imageConstraint.pixelsWide
let expectedHeight = imageConstraint.pixelsHigh
let actualWidth = CVPixelBufferGetWidth(image)
let actualHeight = CVPixelBufferGetHeight(image)
return expectedWidth == actualWidth && expectedHeight == actualHeight
}Multi-Model Resource Exhaustion
When multiple Core ML models run simultaneously — for example, a face detector and a pose estimator operating on the same camera feed — the ANE and GPU memory can be exhausted. This produces "Execution of the command buffer was aborted due to an error" (Metal GPU errors) or "ANE compiler error". The fix is to serialize model execution and release resources between inferences.
// Serial queue prevents concurrent ANE/GPU access
let inferenceQueue = DispatchQueue(label: "com.app.inference", qos: .userInitiated)
inferenceQueue.async {
let faceResult = try? faceModel.prediction(input: faceInput)
// Explicitly release face model resources before running pose model
let poseResult = try? poseModel.prediction(input: poseInput)
}Google ML Kit Crash Patterns
ML Kit provides both on-device and cloud-based APIs for vision, natural language, and custom model inference. The on-device APIs use TensorFlow Lite under the hood, but ML Kit adds its own abstraction layer and lifecycle management that introduces unique crash patterns.
GPU Delegate Failures
ML Kit's object detection and image labeling APIs use GPU acceleration by default for better performance. However, GPU delegates are fragile — they crash when the device's GPU driver has bugs, when OpenCL/OpenGL ES contexts are invalidated during app lifecycle transitions, or when the app is running on an emulator without GPU support.
// ML Kit crashes when GPU delegate fails on certain devices
val options = ObjectDetectorOptions.Builder()
.setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
.build() // GPU delegate selected automatically — may crash
val detector = ObjectDetection.getClient(options)The fix is to detect GPU delegate availability and fall back to CPU.
fun createSafeDetector(): ObjectDetector {
val baseOptions = try {
BaseOptions.Builder().setDelegate(Delegate.GPU).build()
} catch (e: Exception) {
BaseOptions.Builder().setDelegate(Delegate.CPU).build()
}
val options = ObjectDetectorOptions.Builder()
.setBaseOptions(baseOptions)
.setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
.build()
return ObjectDetection.getClient(options)
}On iOS, ML Kit wraps Core ML and Vision frameworks, so crash patterns overlap with Core ML issues. However, ObjectDetector.process() and TextRecognizer.process() must be called from the thread that created the detector, or you get main-thread assertion failures.
Input Validation and Format Errors
ML Kit is strict about input formats. Passing a rotated or null InputImage produces IllegalArgumentException crashes that don't surface during development.
val image = InputImage.fromBitmap(bitmap, rotationDegrees)
recognizer.process(image)
.addOnSuccessListener { /* ... */ }
.addOnFailureListener { e ->
Log.e("MLKit", "Recognition failed: ${e.message}")
}Validate rotation values (0, 90, 180, 270 only) and always attach failure listeners to ML Kit Task APIs — unhandled failures propagate as crashes.
TensorFlow Lite Crash Debugging
TensorFlow Lite is the most flexible of the three frameworks, supporting on-device inference across Android, iOS, embedded Linux, and microcontrollers. That flexibility comes with more configuration surface area — and more things that can crash.
Interpreter Initialization Crashes
The most common TFLite crash is Interpreter initialization failure when the model file is corrupted, uses unsupported ops, or is the wrong format (SavedModel instead of FlatBuffer .tflite). On Android, the crash manifests as java.lang.IllegalArgumentException: Internal error: Failed to create interpreter.
// Safe TFLite initialization with fallback
fun createInterpreter(context: Context, modelName: String): Interpreter? {
return try {
val modelBuffer = FileUtil.loadMappedFile(context, modelName)
val options = Interpreter.Options().apply {
setNumThreads(4)
// Explicitly whitelist ops — avoids "op not found" crashes
addDelegate(NnApiDelegate())
}
Interpreter(modelBuffer, options)
} catch (e: IllegalArgumentException) {
Log.e("TFLite", "Failed to load model: ${e.message}")
null
} catch (e: IllegalStateException) {
Log.e("TFLite", "Interpreter state error: ${e.message}")
null
}
}On iOS, the same crash appears as Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=org.tensorflow.lite. Swift's try! pattern around TFLite Interpreter initialization is a common crash source in production.
NN API Delegate and XNNPACK Failures
TensorFlow Lite on Android uses the Android Neural Networks API (NNAPI) delegate for hardware acceleration. NNAPI delegates are notoriously finicky — certain ops aren't supported, driver bugs on specific SoCs cause silent failures, and quantization type mismatches between the model and the delegate crash the interpreter.
// Defensive NNAPI delegate setup
fun createDelegate(): NnApiDelegate? {
return try {
val delegate = NnApiDelegate()
// Verify the delegate actually initialized
delegate
} catch (e: Exception) {
Log.w("TFLite", "NNAPI delegate unavailable, falling back to CPU")
null
}
}
// Fall back to XNNPACK (CPU) if NNAPI fails
val options = Interpreter.Options().apply {
val nnApiDelegate = createDelegate()
if (nnApiDelegate != null) {
addDelegate(nnApiDelegate)
} else {
setUseXNNPACK(true)
}
}XNNPACK is TFLite's optimized CPU backend, and it is generally more stable than NNAPI. However, certain quantized models (especially those using per-channel quantization or dynamic range quantization) trigger XNNPACK assertion failures that crash the entire process. If you see "Fatal error in XNNPACK" in crash logs, try disabling XNNPACK and falling back to the reference kernel implementation.
options.setUseXNNPACK(false) // Fallback to reference kernels
options.setNumThreads(Runtime.getRuntime().availableProcessors())Thread Safety During Inference
TFLite Interpreter instances are not thread-safe. Calling interpreter.run() from multiple threads simultaneously produces IllegalStateException with cryptic messages like "Input error: Tensor is null" or hard native SIGSEGV crashes. Every Interpreter instance must be accessed from a dedicated thread or protected by a mutex.
// Thread-safe wrapper around TFLite Interpreter
class SafeInterpreter(private val interpreter: Interpreter) {
private val lock = Any()
fun run(input: Array<Any>, output: Map<Int, Any>) {
synchronized(lock) {
interpreter.runForMultipleInputsOutputs(arrayOf(input), output)
}
}
fun close() {
synchronized(lock) {
interpreter.close()
}
}
}Observing On-Device ML Crashes with Bugspulse
On-device ML crashes are hard to reproduce — they depend on ANE generation, GPU driver version, SoC model, model format, and delegate configuration. Bugspulse captures the full context that generic crash reporters miss: which model was loading, which delegate was active, input dimensions, and device capability flags.
Instrument your ML pipeline with custom breadcrumbs before each inference call. On iOS:
Bugspulse.leaveBreadcrumb("Loading CoreML model: ObjectDetector", metadata: [
"model_size_mb": String(modelSize),
"compute_units": "all",
"ios_version": UIDevice.current.systemVersion
])On Android:
Bugspulse.leaveBreadcrumb("TFLite inference started", mapOf(
"model" to "pose_estimation.tflite",
"delegate" to "XNNPACK",
"input_shape" to "1x256x256x3",
"threads" to "4"
))When a crash occurs, Bugspulse ties these breadcrumbs to the report, giving you the full chain of events. Report non-fatal ML errors as handled exceptions to track quality degradation before it becomes a crash:
Bugspulse.recordException(RuntimeException("TFLite inference skipped: NNAPI delegate not available"))Set up crash rate alerts in Bugspulse for ML-related exception types (NSException, IllegalArgumentException, native SIGSEGV in TFLite/CoreML libraries) to catch compatibility issues the moment a new device or OS version triggers them.
Cross-Platform ML Crash Prevention Checklist
Before shipping on-device ML features, verify:
- Model format validation: Verify
.mlmodel,.tflitebundles target your minimum OS version - Delegate fallback chain: CPU fallback when GPU/NNAPI/ANE delegates fail
- Input shape verification: Validate tensor dimensions, pixel formats, and rotation before every call
- Thread confinement: Serialize inference on dedicated queues — never share
Interpreteracross threads - Resource lifecycle: Close
Interpreterand release model objects when the hosting lifecycle ends - Device capability detection: Query compute unit availability before attempting GPU inference
- Quantization compatibility: Test quantized models on all backends — some quantization schemes are delegate-incompatible
- Thermal awareness: Heavy inference triggers throttling that causes ANE/GPU timeouts; implement backpressure
- Breadcrumb instrumentation: Attach model name, delegate type, and input shape to every inference call
- Canary rollout: Release model updates behind feature flags, as recommended in our progressive rollout guide
On-device ML is the fastest-growing category of mobile functionality in 2026. Hardware diversity across iOS and Android means no two deployments behave identically. Systematic crash monitoring with platform-aware breadcrumbs is the difference between shipping with confidence and firefighting after every model update.
Ready to catch on-device ML crashes before your users do? Start your free Bugspulse trial at app.bugspulse.com/register and get ML-aware crash reporting with full breadcrumb context, delegate tracking, and hardware fingerprinting for every device your app runs on.