Skip to content

State handling for static-shape inference engine - #160

Open
carinapeng wants to merge 1 commit into
apple:mainfrom
carinapeng:carina/static-engine-runner-design
Open

State handling for static-shape inference engine#160
carinapeng wants to merge 1 commit into
apple:mainfrom
carinapeng:carina/static-engine-runner-design

Conversation

@carinapeng

Copy link
Copy Markdown
Contributor

Purpose

Static-shape LLM inference engine (StaticShapeEngine) built on the bind(into:) state binding from #156. States are discovered by name and the KV cache is right-sized per context bucket; ctx is parsed from the function name. Input preparation is pluggable per model family via StaticInputProvider + StaticModelProfile; StaticInputContext composes the shared InputContext and the provider mirrors SyncInputHandler.

Testing: LanguageModelsTests (317) pass; correct output on static assets.

@carinapeng
carinapeng force-pushed the carina/static-engine-runner-design branch 3 times, most recently from 50693ed to 35b1494 Compare August 10, 2026 22:57
let desc = try Self.requireDescriptor(model: model, functionName: name)
if Self.contextLength(descriptor: desc, config: configuration) == configuration.maxContextLength {
let ctx =
Self.parseContextLength(functionName: name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there an advantage to introducing this method to get context length from the function name? Or was the original Self.contextLength(descriptor: desc, config: configuration) not sufficient anymore?

let ctx =
Self.parseContextLength(functionName: name)
?? Self.contextLength(descriptor: desc, config: configuration)
guard descriptorsByCtx[ctx] == nil else { continue }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we instead make sure extendFunctionNames is a set at this point, so there's no need for this check? Maybe we can just make the input a set instead of a list.

throw InferenceRuntimeError.invalidState("No extend function to size the state caches from")
}
let ctxLadder = descriptorsByCtx.keys.sorted()
self.stateCtxLadder = ctxLadder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why store this? I don't really see it used

Comment on lines +172 to +173
let smallestCtx = ctxLadder.first!
let largestCtx = ctxLadder.last!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could also just do descriptorsByCtx.keys.min() and .max()

case .float16, .bfloat16: run(Float16.self)
case .float32: run(Float.self)
case .int8: run(Int8.self)
default: run(Float16.self)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this ok default behavior if a different scalar type is passed?

var outputs = try await fn.run(
inputs: inputs,
states: consume states,
states: _unsafeEscapeMutableViews(consume states),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this unavoidable?


var tokenArray = NDArray(descriptor: tokenNDDesc)
let tokenView = tokenArray.mutableView(as: Int32.self)
var tokenView = tokenArray.mutableView(as: Int32.self)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is tokenView modified downstream?

let inputs = Set(desc.inputNames)
let states = Set(desc.stateNames)

// Gemma4: PLE + dual-RoPE + sliding-window KV cache.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are Gemma4 and Qwen3.5 the only models supported in this workflow?

Comment on lines +119 to +144
// metadata.json sits beside the .aimodel. Try the dir + its parent, then scan for it.
var candidates = [
bundleURL.appendingPathComponent("metadata.json"),
bundleURL.deletingLastPathComponent().appendingPathComponent("metadata.json"),
]
for dir in [bundleURL, bundleURL.deletingLastPathComponent()] {
let items =
(try? FileManager.default.contentsOfDirectory(
at: dir, includingPropertiesForKeys: nil)) ?? []
candidates.append(contentsOf: items.filter { $0.lastPathComponent == "metadata.json" })
}
// A model bundle has TWO metadata.json files: the bundle *manifest* (with the
// `language` block we need) beside the .aimodel, and the .aimodel's own internal
// metadata (assetVersion/creationDate/producer, no `language`). When `bundleURL`
// is the .aimodel path, `bundleURL/metadata.json` resolves to the internal one
// first — so pick the first candidate whose JSON actually carries a `language`
// block rather than the first that merely exists.
var lang: [String: Any]? = nil
for url in candidates where FileManager.default.fileExists(atPath: url.path) {
guard let data = try? Data(contentsOf: url),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let l = json["language"] as? [String: Any]
else { continue }
lang = l
break
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be simplified? Every LLM has the same ModelBundle structure so we can just look for the correct metadata.json instead of parsing through candidates?

/// Sliding sub-range `[0, slidingHeadDim)` = GPT-NeoX full rotary (freqs repeated in two halves).
/// Global sub-range = partial rotary: only the first `floor(partialRotaryFactor·globalHeadDim/2)`
/// frequency pairs rotate; the rest are NoPE (θ = 0 ⇒ cos = 1, sin = 0).
final class RoPEProvider: SyncInputHandler {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could use some better test coverage of the classes defined in this file

- Right-size KV cache per context bucket
- Extend shared SyncInputHandler / InputContext (drop Static* fork)
- Unit tests + gemma4/qwen static verified
@carinapeng
carinapeng force-pushed the carina/static-engine-runner-design branch from 35b1494 to 2bdb457 Compare August 12, 2026 03:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants