mirror of
https://github.com/FluidInference/FluidAudio.git
synced 2026-06-11 20:24:36 +00:00
## What Adds an **ANE residency + latency report** (`Documentation/ANE_Profiler.md`) covering every CoreML model pipeline. Per model: **device split** (ANE / GPU / CPU by op, via `MLComputePlan`), **on-disk size**, **type** (streaming vs batch) + **audio chunk size**, and **per-stage latency**. ## Contents - `Documentation/ANE_Profiler.md` — the report (ASR / VAD / Diarization / TTS tables). - `Scripts/ane_profile.swift` — `MLComputePlan` device-split profiler (reproducible; produces the ANE/GPU/CPU numbers). - `Documentation/Models.md` — corrects the Kokoro ANE stage→engine mapping (was backwards: measured ANE stages are Albert / Prosody / Vocoder). **No inference-path changes.** Latency was a one-time real-audio measurement via temporary env-gated timers (since removed); the device split is reproducible via the script. ## Key findings - **ASR**: every encoder is ~97-100% ANE; small decoder/joint/preprocessor sidecars sit on CPU (fine at <30 MB). v3's encoder ships on GPU on purpose. - **Diarization (offline)**: only the 13 MB Embedding net is on ANE; cold-start ANE compile dominates (segmentation 1293 ms cold vs 55 ms warm). - **TTS**: PocketTTS is ~900 ms CPU/utterance (biggest ANE opportunity); Supertonic `VectorEstimator` fails ANE compile; Kokoro ANE stages are Albert/Prosody/Vocoder. ## Notes - ja / zh / CTC-110M latency is synthetic (no transcribe CLI path), flagged in the doc. - Measured on a MacBook (M5); iPhone ANE placement can differ.
133 lines
4.6 KiB
Swift
133 lines
4.6 KiB
Swift
#!/usr/bin/env swift
|
|
// ANE Profiler — reports the CoreML compute-unit *plan* (ANE / GPU / CPU) for each
|
|
// .mlmodelc bundle passed as an argument. Uses MLComputePlan (macOS 14.4+ / iOS 17.4+).
|
|
//
|
|
// Usage: swift Scripts/ane_profile.swift [--units ane|gpu|cpu|all] <path-to.mlmodelc> ...
|
|
//
|
|
// Notes:
|
|
// - This is the scheduler's *plan* (preferredComputeDevice per op), not a runtime power trace.
|
|
// - Counts are by operation (mlprogram) or layer (neuralnetwork). Pipelines recurse into submodels.
|
|
|
|
import CoreML
|
|
import Foundation
|
|
|
|
// Unbuffered stdout so partial results survive a hard CoreML crash (some bundles
|
|
// SIGSEGV/SIGABRT inside MLComputePlan.load — a Swift do/catch can't trap those).
|
|
setvbuf(stdout, nil, _IONBF, 0)
|
|
|
|
struct Counts { var ane = 0, gpu = 0, cpu = 0, other = 0
|
|
var total: Int { ane + gpu + cpu + other }
|
|
static func + (l: Counts, r: Counts) -> Counts {
|
|
Counts(ane: l.ane + r.ane, gpu: l.gpu + r.gpu, cpu: l.cpu + r.cpu, other: l.other + r.other)
|
|
}
|
|
}
|
|
|
|
func classify(_ device: MLComputeDevice, into c: inout Counts) {
|
|
switch device {
|
|
case .neuralEngine: c.ane += 1
|
|
case .gpu: c.gpu += 1
|
|
case .cpu: c.cpu += 1
|
|
@unknown default: c.other += 1
|
|
}
|
|
}
|
|
|
|
@available(macOS 14.4, *)
|
|
func walk(_ ops: [MLModelStructure.Program.Operation], _ plan: MLComputePlan, _ c: inout Counts) {
|
|
for op in ops {
|
|
// const/dummy ops have no device; skip those without a usage entry.
|
|
if let usage = plan.deviceUsage(for: op) {
|
|
classify(usage.preferred, into: &c)
|
|
}
|
|
for block in op.blocks { walk(block.operations, plan, &c) }
|
|
}
|
|
}
|
|
|
|
@available(macOS 14.4, *)
|
|
func profile(_ structure: MLModelStructure, _ plan: MLComputePlan) -> Counts {
|
|
var c = Counts()
|
|
switch structure {
|
|
case .program(let program):
|
|
for (_, fn) in program.functions { walk(fn.block.operations, plan, &c) }
|
|
case .neuralNetwork(let nn):
|
|
for layer in nn.layers {
|
|
if let usage = plan.deviceUsage(for: layer) {
|
|
classify(usage.preferred, into: &c)
|
|
}
|
|
}
|
|
case .pipeline(let pipeline):
|
|
for sub in pipeline.subModels { c = c + profile(sub, plan) }
|
|
case .unsupported:
|
|
break
|
|
@unknown default:
|
|
break
|
|
}
|
|
return c
|
|
}
|
|
|
|
func pct(_ n: Int, _ total: Int) -> String {
|
|
total == 0 ? "—" : String(format: "%2.0f%%", 100.0 * Double(n) / Double(total))
|
|
}
|
|
|
|
// ---- arg parsing ----
|
|
var units: MLComputeUnits = .cpuAndNeuralEngine
|
|
var paths: [String] = []
|
|
var it = CommandLine.arguments.dropFirst().makeIterator()
|
|
while let a = it.next() {
|
|
if a == "--units", let v = it.next() {
|
|
switch v.lowercased() {
|
|
case "ane", "cpuandneuralengine": units = .cpuAndNeuralEngine
|
|
case "gpu", "cpuandgpu": units = .cpuAndGPU
|
|
case "cpu", "cpuonly": units = .cpuOnly
|
|
case "all": units = .all
|
|
default: FileHandle.standardError.write(Data("unknown --units \(v)\n".utf8))
|
|
}
|
|
} else {
|
|
paths.append(a)
|
|
}
|
|
}
|
|
|
|
guard !paths.isEmpty else {
|
|
print("usage: swift Scripts/ane_profile.swift [--units ane|gpu|cpu|all] <model.mlmodelc> ...")
|
|
exit(2)
|
|
}
|
|
|
|
guard #available(macOS 14.4, *) else {
|
|
FileHandle.standardError.write(Data("requires macOS 14.4+\n".utf8))
|
|
exit(1)
|
|
}
|
|
|
|
let config = MLModelConfiguration()
|
|
config.computeUnits = units
|
|
|
|
print(String(format: "%-46@ %5@ %5@ %5@ %@", "model" as NSString, "ANE" as NSString,
|
|
"GPU" as NSString, "CPU" as NSString, "ops"))
|
|
print(String(repeating: "-", count: 78))
|
|
|
|
let sem = DispatchSemaphore(value: 0)
|
|
Task {
|
|
var grand = Counts()
|
|
for path in paths {
|
|
let url = URL(fileURLWithPath: path)
|
|
let name = url.deletingPathExtension().lastPathComponent
|
|
do {
|
|
let plan = try await MLComputePlan.load(contentsOf: url, configuration: config)
|
|
let c = profile(plan.modelStructure, plan)
|
|
grand = grand + c
|
|
print(String(format: "%-46@ %5@ %5@ %5@ %d", name as NSString,
|
|
pct(c.ane, c.total) as NSString, pct(c.gpu, c.total) as NSString,
|
|
pct(c.cpu, c.total) as NSString, c.total))
|
|
} catch {
|
|
print(String(format: "%-46@ load failed: %@", name as NSString,
|
|
"\(error)" as NSString))
|
|
}
|
|
}
|
|
if paths.count > 1 {
|
|
print(String(repeating: "-", count: 78))
|
|
print(String(format: "%-46@ %5@ %5@ %5@ %d", "TOTAL" as NSString,
|
|
pct(grand.ane, grand.total) as NSString, pct(grand.gpu, grand.total) as NSString,
|
|
pct(grand.cpu, grand.total) as NSString, grand.total))
|
|
}
|
|
sem.signal()
|
|
}
|
|
sem.wait()
|