Skip to content

feat: iOS Mobile-First API Improvements — ContextParams.mobile(), memory estimation, isDisposed, chat.cancel() #4

Description

@sebasbad

Summary

While integrating llama_cpp_dart (feat/0.9.0-architecture) into a production iOS app with MediaPipe Gemma + GGUF model switching, several gaps in the public API surfaced that caused crashes and required complex workarounds on the app side. These improvements would make the library significantly safer and more ergonomic for mobile callers.


1. ContextParams.mobile() factory constructor

Problem: Default ContextParams values (nCtx: 4096, nBatch: 2048, nUbatch: 512) are desktop-tuned. Any iOS caller that forgets to override them gets BLAS operator new heap OOM crashes via Apple Accelerate (ggml_backend_blas_graph_compute), which iOS Jetsam kills silently with no stack trace in the app. This is a silent footgun.

Proposed fix:

/// Memory-safe defaults for iOS/Android mobile devices.
/// nCtx=1024, nBatch=128, nUbatch=128, KV cache quantized to q8_0.
factory ContextParams.mobile() => const ContextParams(
  nCtx: 1024,
  nBatch: 128,
  nUbatch: 128,
  typeK: KvCacheType.q8_0,
  typeV: KvCacheType.q8_0,
);

2. LlamaEngine.estimateMemoryBytes() pre-flight check

Problem: ModelParams.noAlloc exists and simulates load without allocating tensors — but this capability is not exposed as a public workflow. Mobile apps need to check required RAM before committing to load a model, especially since iOS Jetsam kills silently with no OOM signal.

Proposed fix:

/// Loads model metadata only (noAlloc: true) and returns estimated
/// peak memory bytes required for the given context params.
/// Throws if the model file doesn't exist or is unreadable.
static Future<int> estimateMemoryBytes({
  required ModelParams modelParams,
  required ContextParams contextParams,
}) async { ... }

This lets mobile apps gate downloads/loads: if (estimate > availableRAM) showWarning().


3. LlamaEngine.isDisposed public getter

Problem: _disposed is private. Callers have no way to check engine liveness before calling createChat(), which throws LlamaLibraryException('LlamaEngine has been disposed.'). This forces callers to maintain their own shadow state tracking.

Proposed fix:

/// True if [dispose] has been called. Subsequent [createChat] calls will throw.
bool get isDisposed => _disposed;

4. EngineChat.cancel() shorthand

Problem: Stopping generation requires the caller to hold and cancel the Dart StreamSubscription — not obvious from the API surface and error-prone (callers may forget to cancel and leak the stream). StopUserAbort exists in the type system but isn't surfaced as a first-class operation.

Proposed fix:

/// Cancels the active generation stream, emitting [StopUserAbort].
/// Safe to call when no generation is in progress (no-op).
Future<void> cancel() async { ... }

5. iOS Metal verification — document LlamaEngine.devices usage

Problem: LlamaEngine.devices, hasAccelerator, and primaryAcceleratorName exist but there is no documented pattern for verifying Metal is actually loaded vs silently falling back to CPU BLAS. On iOS, CPU-only BLAS is 10–20× slower than Metal and will OOM on larger models.

Proposed docs addition:

// After spawn, verify Metal is active:
final engine = await LlamaEngine.spawnFromProcess(...);
if (engine.primaryAcceleratorName != 'Metal') {
  // warn user: running on CPU only — expect slow inference + higher OOM risk
}

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions