Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
af0c489
android: add matched QAIRT graph adapter and build helper
joe-sama Oct 19, 2026
506671f
prefill: JetlinkKit
joe-sama Oct 19, 2026
033ca80
prefill: JetlinkKit/Sources/COrt/include
joe-sama Oct 19, 2026
afb9e54
prefill: JetlinkKit/Sources/COrt
joe-sama Oct 19, 2026
f9e5ea8
prefill: JetlinkKit/Sources/JetlinkAndroid
joe-sama Oct 19, 2026
bed9b72
prefill: JetlinkKit/Sources/JetlinkONNX
joe-sama Oct 19, 2026
b8acd65
prefill: JetlinkKit/Sources/JetlinkORT
joe-sama Oct 19, 2026
61bbb59
prefill: JetlinkKit/Sources/JetlinkServer
joe-sama Oct 19, 2026
085705d
prefill: JetlinkKit/Sources/jetlink-prefill-base
joe-sama Oct 19, 2026
85bf811
prefill: JetlinkKit/Tests/JetlinkServerTests
joe-sama Oct 19, 2026
823edd2
prefill: JetlinkKit/Tests/JetlinkServerTests/Fixtures
joe-sama Oct 19, 2026
5dd2fe0
prefill: android/app
joe-sama Oct 19, 2026
8fd3712
prefill: android/app/src/main/java/io/zoompilot/jetlink/server
joe-sama Oct 19, 2026
2720372
prefill: android/app/src/main/java/io/zoompilot/jetlink/settings
joe-sama Oct 19, 2026
4163e1f
prefill: android/app/src/test/java/io/zoompilot/jetlink/settings
joe-sama Oct 19, 2026
b047935
prefill: docs
joe-sama Oct 19, 2026
8e4da9a
prefill: docs/evidence/sm8750-causal-prefill
joe-sama Oct 19, 2026
911d363
prefill: scripts/android
joe-sama Oct 19, 2026
e681e50
prefill: docs/evidence/sm8750-causal-prefill
joe-sama Oct 19, 2026
39cbd9a
QNN: remove device-pinned research adapter
joe-sama Oct 24, 2026
6b015a9
Docs: keep benchmark evidence linked, outside the diff
joe-sama Oct 24, 2026
e3e8964
Android: portable causal DAG with explicit QNN policy
joe-sama Oct 24, 2026
4fa227c
Android: validate QNN shared allocations and strict sessions
joe-sama Oct 24, 2026
8cc9fa4
Android: apply portable QNN policy at the native bridge
joe-sama Oct 24, 2026
a0ab046
Android: validate causal bundles before model preparation
joe-sama Oct 24, 2026
4f45d6e
Test causal QNN preparation preserves the supported graph
joe-sama Oct 24, 2026
e4a0001
Test portable QNN policy, shared I/O and causal routing
joe-sama Oct 24, 2026
485922a
Android: keep accelerator features opt-in and fingerprint SDK bytes
joe-sama Oct 24, 2026
3c09e0e
Android: select supported model route before preparation
joe-sama Oct 24, 2026
cc248b7
Android: expose opt-in parallel mode on compatible Qualcomm devices
joe-sama Oct 24, 2026
2fd2698
Android: describe supported parallel model and actual fallback
joe-sama Oct 24, 2026
15de7d4
Test Qualcomm parallel availability and whole-NPU fallback
joe-sama Oct 24, 2026
166207b
Android: reduce QNN adapter to the tested activation-fusion workaround
joe-sama Oct 24, 2026
0847054
Document opt-in QNN paths and measured qualification limits
joe-sama Oct 24, 2026
41cc554
Android: compile pinned causal bundle without device-specific gates
joe-sama Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions JetlinkKit/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ let package = Package(
.library(name: "JetlinkLiteRT", targets: ["JetlinkLiteRT"]),
.library(name: "jetlink", type: .dynamic, targets: ["JetlinkAndroid"]),
.executable(name: "jetlink-server", targets: ["jetlink-server"]),
.executable(name: "jetlink-prefill-base", targets: ["jetlink-prefill-base"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", "3.0.0"..<"5.0.0"),
Expand Down Expand Up @@ -125,6 +126,7 @@ let package = Package(
.target(name: "JetlinkLinux", condition: .when(platforms: [.linux])),
.product(name: "ArgumentParser", package: "swift-argument-parser", condition: .when(platforms: [.macOS, .linux])),
]),
.executableTarget(name: "jetlink-prefill-base", dependencies: ["JetlinkONNX"]),
// Helpers more than one test target uses: fixtures, scratch directories, waits, a fake network.
.target(name: "JetlinkTestSupport", dependencies: ["JetlinkRegistry", "JetlinkServer"], path: "Tests/JetlinkTestSupport"),
.testTarget(name: "JetlinkKitTests", dependencies: ["JetlinkKit", "JetlinkUI", "JetlinkTestSupport"], resources: [.copy("Fixtures")]),
Expand Down
12 changes: 11 additions & 1 deletion JetlinkKit/Sources/COrt/include/jl_ort.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum {
typedef struct jl_env jl_env;
typedef struct jl_session jl_session;
typedef struct jl_binding jl_binding;
typedef struct jl_shared_allocator jl_shared_allocator;

typedef struct {
const char *key;
Expand Down Expand Up @@ -77,10 +78,19 @@ char *jl_session_io_info(const jl_session *session, int output, size_t index,
char *name, size_t name_cap, int *elem_type,
int64_t *dims, size_t dims_cap, size_t *rank);

// QNN's host-accessible RPC memory. The session must outlive this allocator,
// and every allocation and binding must be freed before releasing it.
char *jl_shared_allocator_create(jl_session *session, jl_shared_allocator **out);
char *jl_shared_allocator_alloc(jl_shared_allocator *allocator, size_t bytes, void **out);
void jl_shared_allocator_free(jl_shared_allocator *allocator, void *data);
void jl_shared_allocator_release(jl_shared_allocator *allocator);
size_t jl_shared_allocator_total_bytes(const jl_shared_allocator *allocator);

// Binds caller-owned buffers to a session's inputs and outputs once, for
// jl_binding_run to run with. The buffers must outlive the binding.
char *jl_binding_create(jl_session *session, const jl_tensor *inputs, size_t n_inputs,
const jl_tensor *outputs, size_t n_outputs, jl_binding **out);
const jl_tensor *outputs, size_t n_outputs,
const jl_shared_allocator *shared, jl_binding **out);
char *jl_binding_run(jl_binding *binding);
void jl_binding_release(jl_binding *binding);

Expand Down
85 changes: 80 additions & 5 deletions JetlinkKit/Sources/COrt/jl_ort.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include <onnxruntime/onnxruntime_c_api.h>

Expand All @@ -24,6 +25,13 @@ struct jl_binding {
size_t n_values;
};

struct jl_shared_allocator {
OrtSession *session;
OrtAllocator *allocator;
OrtMemoryInfo *memory;
size_t total_bytes;
};

#ifdef JL_ORT_DLOPEN
// NULL when the library or its entry point is missing; jl_env_create says why.
static const OrtApiBase *base(void) {
Expand Down Expand Up @@ -274,14 +282,76 @@ void jl_binding_release(jl_binding *binding) {
free(binding);
}

void jl_shared_allocator_release(jl_shared_allocator *allocator) {
if (allocator == NULL) return;
if (allocator->allocator != NULL) api()->ReleaseAllocator(allocator->allocator);
if (allocator->memory != NULL) api()->ReleaseMemoryInfo(allocator->memory);
free(allocator);
}

char *jl_shared_allocator_create(jl_session *session, jl_shared_allocator **out) {
if (session == NULL || out == NULL) return copy("shared allocator needs a session and output");
*out = NULL;
jl_shared_allocator *allocator = calloc(1, sizeof(*allocator));
if (allocator == NULL) return copy("out of memory");
allocator->session = session->session;
char *error = take(api()->CreateMemoryInfo("QnnHtpShared", OrtDeviceAllocator, 0,
OrtMemTypeDefault, &allocator->memory));
if (error == NULL) error = take(api()->CreateAllocator(session->session, allocator->memory, &allocator->allocator));
const OrtMemoryInfo *actual = NULL;
if (error == NULL) error = take(api()->AllocatorGetInfo(allocator->allocator, &actual));
int different = 1;
if (error == NULL) error = take(api()->CompareMemoryInfo(allocator->memory, actual, &different));
if (error == NULL && different != 0) error = copy("session did not return QNN shared memory");
if (error != NULL) {
jl_shared_allocator_release(allocator);
return error;
}
*out = allocator;
return NULL;
}

char *jl_shared_allocator_alloc(jl_shared_allocator *allocator, size_t bytes, void **out) {
if (allocator == NULL || out == NULL) return copy("invalid shared allocation");
*out = NULL;
if (bytes > SIZE_MAX - allocator->total_bytes) return copy("shared allocation byte count overflow");
char *error = take(api()->AllocatorAlloc(allocator->allocator, bytes, out));
if (error == NULL && *out == NULL) error = copy("QNN shared allocator returned no memory");
if (error == NULL) allocator->total_bytes += bytes;
return error;
}

void jl_shared_allocator_free(jl_shared_allocator *allocator, void *data) {
if (allocator == NULL || data == NULL) return;
char *error = take(api()->AllocatorFree(allocator->allocator, data));
if (error != NULL) {
fprintf(stderr, "Jetlink QNN shared memory free: %s\n", error);
free(error);
}
}

size_t jl_shared_allocator_total_bytes(const jl_shared_allocator *allocator) {
return allocator == NULL ? 0 : allocator->total_bytes;
}

static char *wrap(const OrtMemoryInfo *memory, const jl_tensor *t, OrtValue **out) {
return take(api()->CreateTensorWithDataAsOrtValue(memory, t->data, t->nbytes, t->dims, t->rank,
(ONNXTensorElementDataType)t->elem_type, out));
}

char *jl_binding_create(jl_session *session, const jl_tensor *inputs, size_t n_inputs,
const jl_tensor *outputs, size_t n_outputs, jl_binding **out) {
const jl_tensor *outputs, size_t n_outputs,
const jl_shared_allocator *shared, jl_binding **out) {
*out = NULL;
if (shared != NULL && shared->session != session->session) {
// ORT's QNN allocator tracks RPC allocations globally. A second HTP
// context can register the same host-accessible allocation. Verify that
// this target session really exposes QnnHtpShared; CPU/GPU may not claim it.
jl_shared_allocator *compatible = NULL;
char *error = jl_shared_allocator_create(session, &compatible);
if (error != NULL) return error;
jl_shared_allocator_release(compatible);
}
jl_binding *binding = calloc(1, sizeof(jl_binding));
if (binding == NULL) {
return copy("out of memory");
Expand All @@ -292,8 +362,13 @@ char *jl_binding_create(jl_session *session, const jl_tensor *inputs, size_t n_i
free(binding);
return copy("out of memory");
}
OrtMemoryInfo *memory = NULL;
char *error = take(api()->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &memory));
OrtMemoryInfo *cpu_memory = NULL;
const OrtMemoryInfo *memory = shared != NULL ? shared->memory : NULL;
char *error = NULL;
if (memory == NULL) {
error = take(api()->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &cpu_memory));
memory = cpu_memory;
}
if (error == NULL) {
error = take(api()->CreateIoBinding(session->session, &binding->binding));
}
Expand All @@ -311,8 +386,8 @@ char *jl_binding_create(jl_session *session, const jl_tensor *inputs, size_t n_i
error = take(api()->BindOutput(binding->binding, outputs[i].name, *value));
}
}
if (memory != NULL) {
api()->ReleaseMemoryInfo(memory);
if (cpu_memory != NULL) {
api()->ReleaseMemoryInfo(cpu_memory);
}
if (error != NULL) {
jl_binding_release(binding);
Expand Down
4 changes: 3 additions & 1 deletion JetlinkKit/Sources/JetlinkAndroid/Bridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@
}
return OrtBackend(
profile: profile, preparer: ONNXPreparer(), keepAlive: (config["keep_alive"] as? Bool) ?? true,
keepCPUWarm: (config["keep_cpu_warm"] as? Bool) ?? false, chip: chip)
keepCPUWarm: (config["keep_cpu_warm"] as? Bool) ?? false, chip: chip,
qnn: try QnnPolicy(configuration: config),
causalBundle: (config["parallel_bundle"] as? String).map { URL(fileURLWithPath: $0, isDirectory: true) })
case let other:
throw HostFailure("unknown backend \(other)")
}
Expand Down
7 changes: 5 additions & 2 deletions JetlinkKit/Sources/JetlinkONNX/CoreMLPreparation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public enum CoreMLPreparation {
/// (`--device ane-whole`): model.onnx, with the policy's norms prescaled
/// and the vision heads in fp32.
case aneWhole
/// Offline base for the hash-bound Cinque V3 causal-prefill compiler.
case qnnPrefillBase
/// One model for TensorRT or the CPU: model.onnx, tinygrad's ops
/// stripped and the images fp16, nothing else changed.
case plain
Expand Down Expand Up @@ -119,7 +121,7 @@ public enum CoreMLPreparation {
var tiles = 0
if layout != .plain {
gathers = try Patches.normalizeGatherIndices(&g, src)
gemms = try Patches.gemmWithTransposedWeight(&g, src)
if layout != .qnnPrefillBase { gemms = try Patches.gemmWithTransposedWeight(&g, src) }
tiles = try Patches.expandToTile(&g, src)
}
var norms = 0
Expand All @@ -128,14 +130,15 @@ public enum CoreMLPreparation {
norms = try Patches.prescaleLayerNorm(&g)
heads = try Patches.headsInFP32(&g, src)
}
if layout == .qnnPrefillBase { norms = try QnnPreparation.fusedLayerNorms(&g) }
model.graph = g

var parts: [(name: String, model: Model)]
switch layout {
case .split:
let (vision, policy) = try Split.visionPolicy(model)
parts = [("vision", vision), ("policy", policy)]
case .whole, .aneWhole, .plain:
case .whole, .aneWhole, .qnnPrefillBase, .plain:
parts = [("model", model)]
}
for i in parts.indices where layout != .plain {
Expand Down
51 changes: 51 additions & 0 deletions JetlinkKit/Sources/JetlinkONNX/QnnPreparation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

/// Range-safe fp16 preparation for the validated experimental QNN profile.
public enum QnnPreparation {
/// Keep the native LayerNormalization kernel instead of expanding it into
/// seventeen elementwise/reduction operations. A power-of-two input scale
/// and epsilon/k^2 preserve the real-valued expression; device parity is
/// still required because the HTP's fp16 arithmetic differs from the CPU.
static func fusedLayerNorms(_ graph: inout Graph, k: Float = 4) throws -> Int {
let types = Patches.staticTypes(graph)
let dimensions = Patches.staticDims(graph)
let constant = "__qnn_fused_norm_scale4"
guard !graph.initializers.contains(where: { $0.key == constant }) else {
throw OnnxError("reserved QNN normalization constant already present")
}
var rewritten: [Node] = []
var count = 0
for (index, original) in graph.nodes.enumerated() {
var node = original
if node.op == "LayerNormalization", let input = node.inputs.first, types[input] == DataType.float16 {
guard node.outputs.dropFirst().allSatisfy({ $0.isEmpty }) else {
throw OnnxError("experimental fused normalization does not rewrite optional statistics outputs")
}
let scaled = "__qnn_fused_norm_\(index)"
rewritten.append(Node(inputs: [input, constant], outputs: [scaled], name: scaled, opType: "Mul"))
node.inputs[0] = scaled
let epsilon = (node.attribute("epsilon")?.f ?? 1e-5) / (k * k)
node.attributes.removeAll { $0.name == "epsilon" }
var encoded = Encoded()
encoded.stringField(1, "epsilon")
encoded.tag(2, .fixed32)
let bits = epsilon.bitPattern
for shift in stride(from: 0, to: 32, by: 8) { encoded.byte(UInt8(truncatingIfNeeded: bits >> shift)) }
encoded.intField(20, 1)
node.attributes.append(Attribute(bytes: .owned(encoded.tail), name: "epsilon", type: 1, i: 0, f: epsilon, ints: [], s: "", floats: [], t: nil))
if let shape = dimensions[input] { graph.valueInfo.append(.tensor(scaled, DataType.float16, shape)) }
count += 1
}
rewritten.append(node)
}
if count > 0 {
graph.initializers.append(
Tensor(
name: constant, dims: [], dataType: DataType.float16,
raw: .owned(Elements.encode([1 / k], as: DataType.float16))))
}
graph.nodes = rewritten
return count
}

}
38 changes: 36 additions & 2 deletions JetlinkKit/Sources/JetlinkORT/Ort.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,45 @@ final class OrtSession: @unchecked Sendable {
}
}

/// Host-accessible allocations tracked by QNN for RPC memory registration.
/// Retaining the session keeps its allocator valid until every buffer is freed.
final class OrtSharedAllocator: @unchecked Sendable {
let pointer: OpaquePointer
private let session: OrtSession

init(session: OrtSession) throws {
self.session = session
var allocator: OpaquePointer?
try OrtError.check(jl_shared_allocator_create(session.pointer, &allocator))
guard let allocator else { throw OrtError("QNN returned no shared allocator") }
pointer = allocator
}

deinit { jl_shared_allocator_release(pointer) }

var totalBytes: Int { jl_shared_allocator_total_bytes(pointer) }

var hostAllocator: HostAllocator {
HostAllocator(
allocate: { [self] bytes in
var memory: UnsafeMutableRawPointer?
try OrtError.check(jl_shared_allocator_alloc(pointer, bytes, &memory))
guard let memory else { throw OrtError("QNN shared allocation returned no pointer") }
return memory
},
free: { [self] memory in
jl_shared_allocator_free(pointer, memory)
})
}
}

/// Caller-owned buffers bound to one session's inputs and outputs.
final class OrtBinding {
private let pointer: OpaquePointer
private let sharedAllocator: OrtSharedAllocator?

init(session: OrtSession, inputs: [(TensorSpec, UnsafeMutableRawPointer)], outputs: [(TensorSpec, UnsafeMutableRawPointer)]) throws {
init(session: OrtSession, inputs: [(TensorSpec, UnsafeMutableRawPointer)], outputs: [(TensorSpec, UnsafeMutableRawPointer)], sharedAllocator: OrtSharedAllocator? = nil) throws {
self.sharedAllocator = sharedAllocator
var names: [UnsafeMutablePointer<CChar>?] = []
var dims: [UnsafeMutablePointer<Int64>] = []
defer {
Expand All @@ -135,7 +169,7 @@ final class OrtBinding {
let ins = inputs.map { tensor($0.0, $0.1) }
let outs = outputs.map { tensor($0.0, $0.1) }
var binding: OpaquePointer?
try OrtError.check(jl_binding_create(session.pointer, ins, ins.count, outs, outs.count, &binding))
try OrtError.check(jl_binding_create(session.pointer, ins, ins.count, outs, outs.count, sharedAllocator?.pointer, &binding))
guard let binding else { throw OrtError("onnxruntime returned no binding") }
pointer = binding
}
Expand Down
Loading