Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ let package = Package(
.library(name: "DFlash", targets: ["DFlash"]),
.executable(name: "SwiftLM", targets: ["SwiftLM"]),
.executable(name: "SwiftBuddy", targets: ["SwiftBuddy"]),
.executable(name: "DFlashKernelBench", targets: ["DFlashKernelBench"])
.executable(name: "DFlashKernelBench", targets: ["DFlashKernelBench"]),
.executable(name: "Gemma4MTPBench", targets: ["Gemma4MTPBench"])
],
dependencies: [
// Local Apple MLX Swift fork for C++ extensions
Expand Down Expand Up @@ -53,6 +54,18 @@ let package = Package(
],
path: "Sources/DFlashKernelBench"
),
// ── Gemma4 MTP Speculative Decoding Benchmark ───────────────
.executableTarget(
name: "Gemma4MTPBench",
dependencies: [
"MLXInferenceCore",
.product(name: "MLX", package: "mlx-swift"),
.product(name: "MLXLLM", package: "mlx-swift-lm"),
.product(name: "MLXLMCommon", package: "mlx-swift-lm"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
path: "Sources/Gemma4MTPBench"
),
// ── STFT Audio Profiling Testing Script (macOS only) ───────────
.executableTarget(
name: "SwiftLMTestSTFT",
Expand Down
48 changes: 43 additions & 5 deletions Sources/SwiftLM/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,30 @@ final class ProgressTracker {
}
}

/// Emit one machine-readable JSON-lines event on stdout for the Aegis-AI
/// daemon to consume (`docs/AEGIS_INTEGRATION.md`, and the daemon's own
/// `daemon/spec/engine-protocol.md`). Shared by the `ready` event and the
/// `exiting` event so the JSON-encode-and-flush boilerplate isn't
/// duplicated at each call site — this is deliberately the SAME manual
/// `JSONSerialization` shape the pre-existing `ready` event already used,
/// not a new encoding convention.
///
/// Code-review finding: encoding failure used to be silently swallowed —
/// for a protocol-critical signal the daemon is meant to positively rely
/// on (not just infer), a dropped emit with zero diagnostic trail would be
/// hard to ever notice. Logs to stderr on failure instead.
func emitEvent(_ payload: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: payload),
let json = String(data: data, encoding: .utf8)
else {
FileHandle.standardError.write(
Data("[SwiftLM] failed to encode event for stdout: \(payload)\n".utf8))
return
}
print(json)
fflush(stdout)
}

@main
struct MLXServer: AsyncParsableCommand {
static let configuration = CommandConfiguration(
Expand Down Expand Up @@ -1026,24 +1050,38 @@ struct MLXServer: AsyncParsableCommand {
}
readyEvent["partition"] = info
}
if let data = try? JSONSerialization.data(withJSONObject: readyEvent),
let json = String(data: data, encoding: .utf8) {
print(json)
fflush(stdout)
}
emitEvent(readyEvent)

// ── Graceful shutdown on SIGTERM/SIGINT ──
// Engine Protocol v1 (`daemon/spec/engine-protocol.md` §3, TD-7):
// emit `exiting{reason:"requested"}` before exiting so the daemon
// can tell a planned, no-error stop apart from a real failure —
// `requested` has no `ExitClassification` equivalent on the daemon
// side (a daemon-requested stop never reaches that classifier at
// all), it exists purely for the daemon to positively confirm this
// was a clean shutdown, not infer it from absence of other signals.
let shutdownSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
let interruptSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
signal(SIGTERM, SIG_IGN)
signal(SIGINT, SIG_IGN)
// Code-review finding: if the daemon's read end of our stdout pipe
// is already gone by the time a shutdown signal arrives (e.g. the
// daemon itself already crashed), the exiting-event print()/fflush
// below can raise SIGPIPE — whose default disposition kills this
// process via signal instead of reaching Darwin.exit(0), producing
// exactly the ambiguous "was this a crash?" signature this feature
// exists to eliminate. Ignore SIGPIPE so a closed pipe surfaces as
// an ordinary EPIPE write error instead.
signal(SIGPIPE, SIG_IGN)

shutdownSource.setEventHandler {
print("\n[SwiftLM] Received SIGTERM, shutting down gracefully...")
emitEvent(["event": "exiting", "reason": "requested"])
Darwin.exit(0)
}
interruptSource.setEventHandler {
print("\n[SwiftLM] Received SIGINT, shutting down gracefully...")
emitEvent(["event": "exiting", "reason": "requested"])
Darwin.exit(0)
}
shutdownSource.resume()
Expand Down
24 changes: 24 additions & 0 deletions docs/AEGIS_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ The server will emit a machine-readable JSON ready event on stdout when it is re

Aegis-AI should **wait for this event** before routing any requests to the server.

The server also emits a machine-readable JSON `exiting` event on stdout
when it receives SIGTERM/SIGINT:

```json
{"event":"exiting","reason":"requested"}
```

`reason: "requested"` means *some* SIGTERM/SIGINT was delivered — it does
not distinguish who sent it. The common case is Aegis-AI calling `stop`,
but a manual `kill`/`pkill` or an external process manager produces the
identical event; don't treat `requested` as proof the daemon itself
initiated the shutdown.

This event is **best-effort, not guaranteed**: it's emitted from a signal
handler dispatched on the main queue, so if the main queue is busy (e.g.
mid-request) when the signal arrives, emission is delayed until the queue
frees up — it is not synchronous at signal-delivery time. A consumer that
times out waiting for it and force-kills the process should not treat the
absence of this event as proof the shutdown wasn't requested.

This is the first of what may grow into a small set of self-reported exit
reasons; consumers should ignore any `reason` value they don't recognize
rather than treat it as an error.

---

## 🧠 Running 122B+ MoE Models (Critical)
Expand Down
2 changes: 1 addition & 1 deletion mlx-swift
33 changes: 33 additions & 0 deletions scripts/bootstrap_local_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
# Makes `swift test` runnable locally without CI's help.
#
# A bare `swift test` aborts with "Failed to load the default metallib"
# because Package.swift links MLX but nothing on a local machine ever builds
# or installs mlx.metallib. CI works around this in .github/workflows/ci.yml
# ("Install MLX Metal library" step) by pip-installing the `mlx` wheel and
# copying its bundled metallib into every built .xctest bundle. This script
# does the same thing locally.
set -eo pipefail

VENV_DIR="${MLX_METALLIB_VENV:-/tmp/swiftlm_mlx_venv}"

echo "=> Building test harness (swift build --build-tests)..."
swift build --build-tests

echo "=> Installing MLX Metal library..."
if [ ! -d "$VENV_DIR" ]; then
python3 -m venv "$VENV_DIR"
fi
"$VENV_DIR/bin/pip" install --quiet --upgrade mlx

METALLIB=$(find "$VENV_DIR" -name "mlx.metallib" | head -1)
if [ -z "$METALLIB" ]; then
echo "error: mlx.metallib not found after pip install mlx" >&2
exit 1
fi

cp "$METALLIB" .build/debug/ 2>/dev/null || true
cp "$METALLIB" .build/release/ 2>/dev/null || true
find .build -type d -name "MacOS" -exec cp "$METALLIB" {}/ \;

echo "=> Done. Run tests with: swift test --skip-build"
Loading