Compare commits

..
116 Commits
Author SHA1 Message Date
Georg Bauer e32954ac1b Add Qwen vision loading and Metal inference 2026-09-11 18:32:41 +02:00
Georg Bauer 48c2f751b4 Align DeepSeek and GLM execution with DS4 2026-09-11 17:18:45 +02:00
Georg Bauer 02db0968ae Save inference parity implementation and evaluation harness 2026-09-10 22:45:59 +02:00
Georg Bauer b99ce2aa10 fix for stats and model selection
Weekly OSV dependency audit / dependency-audit (push) Failing after 5s
2026-09-05 08:17:07 +02:00
Georg Bauer bd6353804b Optimize Qwen3.8 inference on Apple silicon 2026-09-04 23:27:09 +02:00
Georg Bauer bf82df77cb Add native Qwen MTP speculation 2026-09-03 22:38:52 +02:00
Georg Bauer 714b39f7f3 Implement exact Qwen sparse attention 2026-09-03 21:45:39 +02:00
Georg Bauer 87ccf67d0c Stream Qwen PLE embeddings 2026-09-03 21:12:20 +02:00
Georg Bauer c414640050 Implement Qwen text core 2026-09-03 20:31:53 +02:00
Georg Bauer 3773cfda2e Integrate Qwen3.8 model intake 2026-09-03 19:56:23 +02:00
Georg Bauer 4c83ac0360 Prepare Qwen3.8 artifact set 2026-09-03 19:15:53 +02:00
Georg Bauer 9556b26c55 Bind sessions to their selected model 2026-09-01 22:52:01 +02:00
Georg Bauer 35f306bf2a Lock tool sessions to their model 2026-09-01 22:37:57 +02:00
Georg Bauer d40e86e5ef correct thinking modes for each model 2026-09-01 21:03:13 +02:00
Georg Bauer ee515ee824 Load GLM vision weights on demand 2026-09-01 20:08:36 +02:00
Georg Bauer 46d6a976a5 Add GLM 5.3 Flash support 2026-09-01 19:06:20 +02:00
Georg Bauer 9a33c61ea6 Remove the canceled Mojo spike 2026-08-31 17:30:55 +02:00
Georg Bauer 0dcf181dbe Establish the Mojo 1.0 integration gate 2026-08-31 15:25:04 +02:00
Georg Bauer 2c2c3702b0 Add Cargo dependency audit gates 2026-08-31 14:26:51 +02:00
Georg Bauer 115737302b Move Mojo roadmap to Gitea 2026-08-31 14:22:48 +02:00
Georg Bauer 85138c49d4 Add standalone session exports 2026-08-31 14:07:29 +02:00
Georg Bauer 5c74739f65 Add native glob file discovery 2026-08-31 08:44:44 +02:00
Georg Bauer 8adac261ae Add durable inline SVG presentation 2026-08-31 08:29:24 +02:00
Georg Bauer 0fa15bb68b Remove deprecated Flash preview and legacy MTP 2026-08-31 08:04:20 +02:00
Georg Bauer 7f88e23884 Make F1 open the complete User Guide 2026-08-31 07:43:29 +02:00
Georg Bauer 32c3a1a47a Use login shell PATH for extension hooks
Weekly OSV dependency audit / dependency-audit (push) Failing after 5s
2026-08-30 22:40:57 +02:00
Georg Bauer 6c4e792c8a Accelerate DeepSeek DSpark inference 2026-08-30 21:45:16 +02:00
Georg Bauer 28231e1faf Add end-to-end MXFP4 Metal support 2026-08-30 20:42:23 +02:00
Georg Bauer 3977261bd3 Add installable agent lifecycle extensions 2026-08-30 16:05:09 +02:00
Georg Bauer 1ac559bbd0 Reach DS4 Metal performance parity 2026-08-30 15:26:27 +02:00
Georg Bauer 79468c65b6 Expand detail views with the sidebar hidden 2026-08-30 13:25:45 +02:00
Georg Bauer f1b62c65f9 Update agent feature documentation 2026-08-30 13:19:51 +02:00
Georg Bauer 1597ddbc11 Add hierarchical workspace instructions 2026-08-29 21:57:11 +02:00
Georg Bauer e35f57bc83 Build tagged macOS releases 2026-08-29 21:22:58 +02:00
Georg Bauer c0681dedb9 Integrate standard agent skills 2026-08-29 21:12:40 +02:00
Georg Bauer f2772f20c5 Add fresh-agent Ralph loops 2026-08-29 21:05:07 +02:00
Georg Bauer f12aafcb2e Make agent tools schema-driven 2026-08-29 20:43:56 +02:00
Georg Bauer f1c177b754 Support DeepSeek V4 Flash 0731 2026-08-29 20:28:50 +02:00
Georg Bauer ad855b321e workover of the mojo plan 2026-08-29 18:53:28 +02:00
Georg Bauer 5e179a0f2c thinking about mojo 2026-08-29 14:33:03 +02:00
Georg Bauer 02022f4fca Fix dependency audit findings
Weekly OSV dependency audit / dependency-audit (push) Failing after 4s
2026-08-09 16:41:00 +02:00
Georg Bauer 39ee904b3b Add weekly OSV dependency audit 2026-08-09 16:26:26 +02:00
Georg Bauer f62a706f12 Trust Dev Brain paths during shell approval 2026-08-02 14:46:26 +02:00
Georg Bauer e52efb80b0 Store preferences by model profile 2026-08-02 12:38:21 +02:00
Georg Bauer 69c83e7059 Improve chat Markdown exports 2026-08-02 10:38:24 +02:00
Georg Bauer 0b4e4fcc4a Load shell environment once at startup 2026-08-02 10:10:17 +02:00
Georg Bauer 118a9d41e0 feat: record agent approvals 2026-08-02 09:47:28 +02:00
Georg Bauer 94ed73e235 feat: keep track of window size and position 2026-08-01 21:01:43 +02:00
Georg Bauer c0e4ec13cc Select thinking mode from the chat 2026-08-01 09:49:37 +02:00
Georg Bauer d0431edaff Use the user login shell for agent commands 2026-08-01 09:40:55 +02:00
Georg Bauer d1c380f79d feat: DeepSeek V4 Flash 0731 added 2026-08-01 08:40:06 +02:00
Georg Bauer 4175706533 feat: a skill creating skill as default 2026-07-30 13:41:04 +02:00
Georg Bauer f2fb8d79f7 fix: stricter dev brain validation 2026-07-30 10:21:27 +02:00
Georg Bauer 30b305bc22 fix: better validate messages 2026-07-30 10:05:00 +02:00
Georg Bauer 73d7c8af27 feat: AI based permission checks 2026-07-30 09:47:54 +02:00
Georg Bauer c101ca1380 chore: removed old plan document 2026-07-29 19:29:00 +02:00
Georg Bauer 92f351ae0b chore: refactorings for more code reorganizing 2026-07-29 19:28:28 +02:00
Georg Bauer 95ba6c3c76 chore: updated refactor plan 2026-07-29 19:14:15 +02:00
hugo 6387538da9 Merge pull request 'Refactor application and runtime boundaries' (#61) from linux-refactoring-base into main
Reviewed-on: #61
2026-07-29 16:48:38 +00:00
Georg Bauer 83905690e6 Fix refactor recovery and metrics 2026-07-29 18:47:14 +02:00
Hermes Agent 607aaa2e7a Restore DSML tool-call recovery 2026-07-29 11:00:31 +00:00
Hermes Agent 0f0a1a5409 Refactor application and runtime boundaries 2026-07-29 10:40:39 +00:00
Hermes Agent 902037f947 Support Linux refactoring checks 2026-07-29 10:21:56 +00:00
Hermes Agent 36f1056cf7 Document refactoring opportunities 2026-07-29 07:08:03 +00:00
Georg Bauer d8b29fb1d1 fix: hopefully fixed a2ui confusion by the model 2026-07-28 22:36:16 +02:00
Georg Bauer 39c4181ae2 chore: removed reference to old license for stuff we don't use anymore 2026-07-28 22:19:40 +02:00
Georg Bauer 9d1c403ae7 Replace C browser client with headless Chrome 2026-07-28 22:17:45 +02:00
Georg Bauer 9683d5d10b make a2ui usage more clear to prevent every-session-spam 2026-07-28 21:37:05 +02:00
Georg Bauer dbccaf7088 fix: fixed git handling 2026-07-28 21:11:46 +02:00
Georg Bauer 9b5b3a2f6a fix: less fragile dev brain sources revisions 2026-07-28 20:47:44 +02:00
Georg Bauer ed8f7c4a01 fix: fixes to agent behaviour and llm steering 2026-07-28 20:07:41 +02:00
Georg Bauer 1f028950b3 fix: better sidebar layout 2026-07-28 19:40:31 +02:00
Georg Bauer eaa2fcfe42 Discard archived session checkpoints 2026-07-28 19:20:10 +02:00
Georg Bauer 74c5948955 Align session titles in the sidebar 2026-07-28 19:11:23 +02:00
Georg Bauer 191ff0b2ce Persist the default Git diff layout 2026-07-28 19:07:59 +02:00
Georg Bauer 07c89094b4 Add current session statistics 2026-07-28 19:04:02 +02:00
Georg Bauer 44d614378f Group chat token counts by thousands 2026-07-28 18:59:07 +02:00
Georg Bauer 2391e2a680 Add verified Dev Brain skills 2026-07-28 18:57:08 +02:00
Georg Bauer 5ec873074b Add A2UI heatmap charts 2026-07-28 18:44:45 +02:00
Georg Bauer abac978f54 Document managed model storage 2026-07-28 18:39:37 +02:00
Georg Bauer 00af0335ff Use slider toggles for boolean controls 2026-07-28 18:38:30 +02:00
Georg Bauer 9cf1c70a67 Keep runtime files inside application support 2026-07-28 18:34:08 +02:00
Georg Bauer 31f6426eef Replace gix with configurable libgit2 diffs 2026-07-28 18:25:24 +02:00
Georg Bauer ed2226bc60 Replace Git subprocesses with native APIs 2026-07-28 06:55:05 +02:00
Georg Bauer 90a445cafd fix: better validate for dev_brain and 50k reminder not triggering rebuild 2026-07-27 23:08:47 +02:00
Georg Bauer d371d9d659 Render tool output as terminal text 2026-07-27 22:07:21 +02:00
Georg Bauer a7f26ccb71 Fix Git diff rendering 2026-07-27 22:01:24 +02:00
Georg Bauer 6001e7312b fix: better handling of dev brain 2026-07-27 21:42:58 +02:00
Georg Bauer 9a55a49b00 chore: removed old plan that was finished 2026-07-27 21:17:05 +02:00
Georg Bauer cfc4fce8d7 feat: first cut at dev brain 2026-07-27 21:16:10 +02:00
Georg Bauer 56be87d288 Add pane shortcuts to View menu 2026-07-27 20:14:19 +02:00
Georg Bauer 9d58f79295 Fix Git action bar sizing 2026-07-27 20:04:21 +02:00
Georg Bauer b9933ae076 Add Git worktree pane 2026-07-27 19:44:12 +02:00
Georg Bauer aebfe7aeb6 Add project and branch chat controls 2026-07-27 19:20:57 +02:00
Georg Bauer 7d865df20f Run chats independently 2026-07-27 19:12:59 +02:00
Georg Bauer 22a939751e Keep chat scrolling under user control 2026-07-27 18:58:45 +02:00
Georg Bauer 7f14b6008a Order sessions by recent use 2026-07-27 18:53:39 +02:00
Georg Bauer 1017087fc9 fix: better A2UI description 2026-07-27 18:49:06 +02:00
Georg Bauer 4adb49a4d1 Reuse canonical chat context 2026-07-27 18:35:52 +02:00
Georg Bauer ee7e5696a5 chore: updated plan for dev brain 2026-07-27 17:08:13 +02:00
Georg Bauer 50ae1cd5b9 fix: markdown rendering for chat was broken 2026-07-27 16:38:48 +02:00
Georg Bauer c4e92f6206 Add preference section navigation 2026-07-27 15:26:25 +02:00
Georg Bauer c96675c462 feat: more native menus with expected reactions 2026-07-27 15:09:12 +02:00
Georg Bauer 8f02f0934b Improve the default system prompt 2026-07-27 14:26:10 +02:00
Georg Bauer a90a241ae1 Confirm session deletion 2026-07-27 14:18:32 +02:00
Georg Bauer 8a08cd7b52 Improve chat rendering and generation summaries 2026-07-27 14:04:14 +02:00
Georg Bauer 6a541b51f9 fix: rendering in a2ui surface and viewport aligned 2026-07-27 13:53:01 +02:00
Georg Bauer f69623ad02 feat: moved a2ui surface to a separate panel 2026-07-27 12:58:46 +02:00
Georg Bauer ef1c574a7f fix: better hardening of tool calls 2026-07-27 12:12:35 +02:00
Georg Bauer 4a309091b6 prev/next A2UI surface and better dismissal 2026-07-27 11:52:15 +02:00
Georg Bauer c9c2d8efd5 feat: a2ui interface to enablee the LLM to give structured information 2026-07-27 11:19:57 +02:00
Georg Bauer 27b10ee0e1 chore: more on menus 2026-07-27 07:42:59 +02:00
Georg Bauer 61c2028e27 chore: reduce plan to bare minimum - no issues 2026-07-27 07:36:07 +02:00
Georg Bauer 21b9223706 chore: updated PLAN 2026-07-26 22:28:59 +02:00
Georg Bauer 47b405e72b fix: ssd streaming and deepseek fixes 2026-07-26 21:50:59 +02:00
Georg Bauer 0d80c217c4 Finish DS4 execution parity 2026-07-26 20:44:23 +02:00
318 changed files with 207804 additions and 9711 deletions
+21
View File
@@ -0,0 +1,21 @@
[advisories]
ignore = [
# Unmaintained transitive dependencies with no patched version.
"RUSTSEC-2020-0095", # difference via the release-only apple-codesign crate
"RUSTSEC-2024-0436", # paste via Iced's wgpu Metal backend
"RUSTSEC-2025-0134", # rustls-pemfile via apple-codesign's unused remote-signing stack
"RUSTSEC-2026-0192", # ttf-parser via Iced's text and SVG renderers
"RUSTSEC-2026-0206", # rustybuzz via Iced's SVG renderer
# No compatible upgrade exists; Cryoglyph does not call the affected LruCache::pop API.
"RUSTSEC-2026-0253",
# No patch exists; the release packager only creates ad-hoc signatures without RSA keys.
"RUSTSEC-2023-0071",
]
[output]
deny = ["warnings"]
format = "terminal"
quiet = false
show_tree = true
+2
View File
@@ -0,0 +1,2 @@
[env]
MACOSX_DEPLOYMENT_TARGET = "15.0"
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""Create and publish Gitea releases for tagged DS4Server builds."""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
import unittest
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
def issue_numbers(message: str) -> list[int]:
return sorted({int(number) for number in re.findall(r"#(\d+)", message)})
def markdown(text: str) -> str:
return text.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")
def release_body(issues: list[dict[str, object]], server: str, repository: str, sha: str) -> str:
groups = {"Fixes": [], "Improvements": [], "Other changes": []}
for issue in issues:
labels = {label["name"] for label in issue.get("labels", [])} # type: ignore[index]
group = "Fixes" if "bug" in labels else "Improvements" if "enhancement" in labels else "Other changes"
title = " ".join(str(issue["title"]).split())
groups[group].append(f"- [#{issue['number']} {markdown(title)}]({issue['html_url']})")
sections = [f"## {name}\n" + "\n".join(items) for name, items in groups.items() if items]
if not sections:
sections.append("No closed issues were linked from commits in this release.")
sections.append(f"Built from [{sha[:12]}]({server}/{repository}/commit/{sha}).")
return "\n\n".join(sections)
class Gitea:
def __init__(self) -> None:
self.api_url = os.environ["GITEA_API_URL"].rstrip("/")
self.repository = os.environ["GITEA_REPOSITORY"]
self.token = os.environ["GITEA_TOKEN"]
def request(self, method: str, path: str, payload: object | None = None) -> object:
data = None if payload is None else json.dumps(payload).encode()
request = Request(
f"{self.api_url}{path}",
data=data,
method=method,
headers={"Authorization": f"token {self.token}", "Content-Type": "application/json"},
)
with urlopen(request) as response:
content = response.read()
return None if not content else json.loads(content)
def issue(self, number: int) -> dict[str, object]:
return self.request("GET", f"/repos/{self.repository}/issues/{number}") # type: ignore[return-value]
def commit_messages(sha: str) -> str:
try:
previous = subprocess.check_output(
["git", "describe", "--tags", "--abbrev=0", f"{sha}^"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
except subprocess.CalledProcessError:
previous = ""
revision = f"{previous}..{sha}" if previous else sha
return subprocess.check_output(["git", "log", "--format=%s%n%b", revision], text=True)
def prepare() -> None:
gitea = Gitea()
tag = os.environ["GITEA_REF_NAME"]
sha = os.environ["GITEA_SHA"]
server = os.environ["GITEA_SERVER_URL"].rstrip("/")
issues = [gitea.issue(number) for number in issue_numbers(commit_messages(sha))]
closed = [issue for issue in issues if issue["state"] == "closed"]
payload = {
"tag_name": tag,
"target_commitish": sha,
"name": tag,
"body": release_body(closed, server, gitea.repository, sha),
"draft": True,
"prerelease": False,
}
try:
release = gitea.request("GET", f"/repos/{gitea.repository}/releases/tags/{quote(tag, safe='')}")
except HTTPError as error:
if error.code != 404:
raise
release = gitea.request("POST", f"/repos/{gitea.repository}/releases", payload)
else:
if not release["draft"]: # type: ignore[index]
raise RuntimeError(f"release {tag} is already published")
release = gitea.request("PATCH", f"/repos/{gitea.repository}/releases/{release['id']}", payload) # type: ignore[index]
for asset in release["assets"] or []: # type: ignore[index]
gitea.request(
"DELETE",
f"/repos/{gitea.repository}/releases/{release['id']}/assets/{asset['id']}", # type: ignore[index]
)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"release_id={release['id']}\n") # type: ignore[index]
def publish() -> None:
gitea = Gitea()
release_id = int(os.environ["RELEASE_ID"])
gitea.request("PATCH", f"/repos/{gitea.repository}/releases/{release_id}", {"draft": False})
class ReleaseTests(unittest.TestCase):
def test_notes_are_unique_sorted_grouped_and_linked(self) -> None:
self.assertEqual(issue_numbers("Fix #12, refs #3 and #12"), [3, 12])
body = release_body(
[
{
"number": 12,
"title": "Fix [unlock]",
"html_url": "https://example.test/issues/12",
"labels": [{"name": "bug"}],
},
{
"number": 3,
"title": "Add search",
"html_url": "https://example.test/issues/3",
"labels": [{"name": "enhancement"}],
},
],
"https://example.test",
"owner/repo",
"1234567890abcdef",
)
self.assertIn("## Fixes\n- [#12 Fix \\[unlock\\]](https://example.test/issues/12)", body)
self.assertIn("## Improvements\n- [#3 Add search](https://example.test/issues/3)", body)
self.assertIn("[1234567890ab](https://example.test/owner/repo/commit/1234567890abcdef)", body)
if __name__ == "__main__":
command = sys.argv[1] if len(sys.argv) == 2 else ""
if command == "prepare":
prepare()
elif command == "publish":
publish()
elif command == "test":
unittest.main(argv=[sys.argv[0]])
else:
raise SystemExit("usage: release.py prepare|publish|test")
+31
View File
@@ -0,0 +1,31 @@
name: Weekly OSV dependency audit
on:
schedule:
# Runs every Monday. Change the minute to stagger repositories.
- cron: "41 3 * * 1"
workflow_dispatch:
jobs:
dependency-audit:
runs-on: linux-arm64
timeout-minutes: 20
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-audit@0.22.2
- name: Audit Rust dependencies
run: |
cargo audit
cargo audit --file tools/macos-packager/Cargo.lock
- name: Scan dependencies for known vulnerabilities
uses: docker://ghcr.io/google/osv-scanner:v2
with:
args: scan source --recursive .
+108
View File
@@ -0,0 +1,108 @@
name: Tagged release
on:
push:
tags:
- "*"
env:
CARGO_TERM_COLOR: always
jobs:
prepare-release:
runs-on: linux-arm64
outputs:
release_id: ${{ steps.release.outputs.release_id }}
steps:
- name: Check out the tag
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-audit@0.22.2
- name: Audit Rust dependencies
run: |
cargo audit
cargo audit --file tools/macos-packager/Cargo.lock
- name: Test release-note generation
run: python3 .gitea/scripts/release.py test
- name: Create draft release
id: release
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_REF_NAME: ${{ gitea.ref_name }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_SHA: ${{ gitea.sha }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: python3 .gitea/scripts/release.py prepare
build-macos-arm64:
needs: prepare-release
runs-on: linux-arm64
container: ghcr.io/rust-cross/cargo-zigbuild@sha256:82af75c41958c2af2787e8bedd912da7678a9438937e223e9d83d006d747b38b
steps:
- name: Check out the tag
env:
GITEA_REF_NAME: ${{ gitea.ref_name }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -eu
git init
git -c http.extraHeader="Authorization: token $GITEA_TOKEN" fetch --depth=1 \
"$GITEA_SERVER_URL/$GITEA_REPOSITORY.git" "refs/tags/$GITEA_REF_NAME"
git checkout --detach FETCH_HEAD
- name: Build and package the Apple Silicon application
env:
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS: -C link-arg=-Wl,-headerpad,0x1000
GITEA_REF_NAME: ${{ gitea.ref_name }}
run: |
set -eu
version=$(printf '%s' "$GITEA_REF_NAME" | sed 's/[^A-Za-z0-9._-]/-/g')
asset="dist/DS4Server-${version}-darwin-arm64.dmg"
cargo zigbuild --release --locked --target aarch64-apple-darwin --bin ds4-server
cargo run --release --locked --manifest-path tools/macos-packager/Cargo.toml -- \
"$GITEA_REF_NAME" target/aarch64-apple-darwin/release/ds4-server "$asset"
- name: Upload the DMG
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_ID: ${{ needs.prepare-release.outputs.release_id }}
run: |
set -eu
asset=$(find dist -maxdepth 1 -type f -name '*.dmg' -print -quit)
test -n "$asset"
name=$(basename "$asset")
curl --fail --silent --show-error --retry 3 \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$asset" \
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/releases/$RELEASE_ID/assets?name=$name"
publish-release:
needs:
- prepare-release
- build-macos-arm64
runs-on: linux-arm64
steps:
- name: Check out the tag
uses: actions/checkout@v4
- name: Publish complete release
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_ID: ${{ needs.prepare-release.outputs.release_id }}
run: python3 .gitea/scripts/release.py publish
+1
View File
@@ -1,2 +1,3 @@
/target /target
/local-eval-results
.DS_Store .DS_Store
+3
View File
@@ -3,6 +3,8 @@
- Prefer simple, idiomatic Rust; reuse existing code and dependencies before adding abstractions or crates. - Prefer simple, idiomatic Rust; reuse existing code and dependencies before adding abstractions or crates.
- Keep changes focused, handle errors explicitly, and add the smallest useful test for non-trivial behavior. - Keep changes focused, handle errors explicitly, and add the smallest useful test for non-trivial behavior.
- Preserve `rustfmt` output and keep Clippy warning-free. - Preserve `rustfmt` output and keep Clippy warning-free.
- Treat DS4 as the behavioral oracle. Model execution, token processing, context accounting, and KV-cache behavior must remain identical to DS4. Differences are bugs unless they are unavoidable Rust/platform requirements and the user explicitly approves them before implementation; when uncertain, preserve DS4 behavior.
- Keep all application, model-loading, inference-graph, session, and orchestration code in Rust. Never copy, vendor, compile, or call DS4's C implementation; use it only as a behavioral and performance reference. Native code is limited to the Objective-C Metal bridge and `.metal` kernels required to execute GPU work.
- This is not a GitHub project. Use direct `git` commands for version control and the `tea` CLI for forge operations; do not use GitHub tools or workflows. - This is not a GitHub project. Use direct `git` commands for version control and the `tea` CLI for forge operations; do not use GitHub tools or workflows.
- Issues are managed with the command "tea" run from the project directory. - Issues are managed with the command "tea" run from the project directory.
@@ -11,6 +13,7 @@
- Follow the Codex macOS dark UI: near-black backgrounds, subtly raised surfaces, quiet borders, rounded corners, and restrained contrast. - Follow the Codex macOS dark UI: near-black backgrounds, subtly raised surfaces, quiet borders, rounded corners, and restrained contrast.
- Present overviews as one bordered panel with divided rows, a clear primary label, muted supporting text, and compact trailing actions. - Present overviews as one bordered panel with divided rows, a clear primary label, muted supporting text, and compact trailing actions.
- Reuse the shared surface and action-button styles in `src/app/view.rs`; keep button shape, padding, typography, hover, and disabled states consistent across windows. Destructive actions may differ by color only, while navigation controls may remain flat. - Reuse the shared surface and action-button styles in `src/app/view.rs`; keep button shape, padding, typography, hover, and disabled states consistent across windows. Destructive actions may differ by color only, while navigation controls may remain flat.
- Render boolean controls as macOS-style slider toggles through the shared `toggle` helper, not checkboxes.
- Prefer generous spacing and clear hierarchy over decoration; avoid one-off colors, card stacks, oversized controls, and screen-specific button styling. - Prefer generous spacing and clear hierarchy over decoration; avoid one-off colors, card stacks, oversized controls, and screen-specific button styling.
## Commit gates ## Commit gates
Generated
+602 -1585
View File
File diff suppressed because it is too large Load Diff
+27 -5
View File
@@ -3,6 +3,7 @@ name = "ds4-server"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
rust-version = "1.97" rust-version = "1.97"
default-run = "ds4-server"
description = "A native macOS coding-agent GUI for DwarfStar" description = "A native macOS coding-agent GUI for DwarfStar"
license = "MIT" license = "MIT"
publish = false publish = false
@@ -12,21 +13,34 @@ build = "build.rs"
cc = "1.3.0" cc = "1.3.0"
[dependencies] [dependencies]
base64 = "0.22.1"
diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] } diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] }
diesel_migrations = "2.3.2" diesel_migrations = "2.3.2"
iced = { version = "0.14.0", features = ["advanced", "highlighter", "markdown", "svg", "tokio"] } git2 = { version = "0.21.0", features = ["https", "vendored-libgit2", "vendored-openssl"] }
headless_chrome = "1.0.22"
iced = { version = "0.14.0", default-features = false, features = ["advanced", "image-without-codecs", "markdown", "svg", "tokio", "wgpu"] }
image = { version = "0.25.10", default-features = false, features = ["gif", "jpeg", "png", "webp"] }
libc = "0.2.186"
memmap2 = "0.9.11" memmap2 = "0.9.11"
png = "0.17.16" png = "0.17.16"
rfd = "0.15.4" pulldown-cmark = "0.12.2"
regex = "1.13.1"
rfd = { version = "0.15.4", default-features = false }
rustc-hash = "2.1.3"
self_cell = "1.3.0"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] } serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
serde_norway = "0.9.42" serde_norway = "0.9.42"
sha2 = "0.11.0" sha2 = "0.11.0"
shlex = "2.0.1"
time = { version = "0.3.54", features = ["formatting", "parsing"] }
turbovault-parser = "1.6.0"
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] } ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
url = "2.5.8" url = "2.5.8"
usvg = "0.45.1"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
muda = "0.15.3" muda = { version = "0.19.3", default-features = false }
[package.metadata.packager] [package.metadata.packager]
product-name = "DS4Server" product-name = "DS4Server"
@@ -34,7 +48,15 @@ identifier = "de.rfc1437.ds4server"
description = "A native macOS coding-agent GUI for DwarfStar" description = "A native macOS coding-agent GUI for DwarfStar"
binaries = [{ path = "ds4-server", main = true }] binaries = [{ path = "ds4-server", main = true }]
icons = ["assets/DS4Server.icns", "assets/app-icon.png"] icons = ["assets/DS4Server.icns", "assets/app-icon.png"]
resources = ["metal"] resources = ["metal", "assets/dev-brain"]
[package.metadata.packager.macos] [package.metadata.packager.macos]
minimum-system-version = "13.0" minimum-system-version = "15.0"
[[bin]]
name = "qwen38-artifacts"
path = "tools/qwen38-artifacts.rs"
[[bin]]
name = "test-supervisor"
path = "tools/test-supervisor.rs"
-307
View File
@@ -1,307 +0,0 @@
# DS4Server implementation plan
Bundle/application identifier: `DS4Server.rfc1437.de`
DS4Server is the native macOS Rust/Iced application counterpart to `../ds4`.
The reference implementation remains normative: `ds4.c`/`ds4.h` define model
and session behavior, `ds4_server.c` defines the HTTP API, and `ds4_agent.c`
defines local-agent behavior and tools. Parity means matching observable
behavior and fixtures, not merely exposing similar controls. All planned
execution targets one self-contained Mac.
## Current state
- The macOS application shell, projects, session rename/pin/archive/delete,
SQLite transcripts, model preferences and downloads, Stats dashboard, native
Application/Edit/Window menu integration, development bundle, and shared lazy
model lifecycle are implemented.
- DeepSeek V4 Flash runs through the Rust/Metal executor with full configured
context, ratio-4 sparse attention, cold and resumed batched prefill, prefix
continuation, durable KV checkpoints, cancellation, and idle unload.
- The configurable localhost endpoint implements all `ds4_server.c` routes,
streaming and non-streaming envelopes, model aliases, CORS, tool replay and
continuation, malformed DSML recovery, transient prefix-aware KV reuse,
disconnect/queue cancellation, and shared model scheduling. The full
automated C/Rust differential corpus remains open.
- Local sessions now match the long-running `ds4_agent.c` baseline: the full
starting tool set, unlimited tool rounds, queued user guidance between tool
rounds, session date/time context, periodic tool-contract reminders,
cooperative Stop, and explicit activity/failure states are implemented.
- Local tools are hardened for daily use: canonical project boundaries reject
parent and symlink escapes, shell commands receive a deliberate environment,
risky shell and visible-browser actions share one cancellable Allow once/Deny
approval path, and compact tool cards expose bounded parameters, results, and
parsing/approval/queue/run/completion lifecycle state without showing DSML.
Background jobs and bounded output files stop and clean up with Stop, session
switches, and application shutdown.
- Context compaction uses the reference soft and exact token-counted hard
triggers, private live-model summaries, bounded summary and tool-result
retries, a recent verbatim tail, running-job observations, and compatible KV
checkpoints. Every summary is a visible durable history marker carrying its
tail boundary; the full chat remains scrollable, while missing or
incompatible KV state rebuilds from the last marker, its tail, and later
messages. Manual compact is available after new chat following the latest
marker, alongside a checkpoint-discard/rebuild action.
- Focused coverage exercises triggers, summary bounds and sanitizing, tail
selection, queued guidance, checkpoint identity, running jobs, durable
compaction markers, relaunch, and continued tool work after rebuild.
- DeepSeek V4 Flash now supports DS4-compatible SSD expert streaming, legacy
MTP, DSpark, and directional steering in the Rust executor. The optional
modes are integrated into the same target-owned generation path used by
local chat and the endpoint; disabling them preserves the resident greedy
token baseline. Runtime counters feed the Stats dashboard without inference
thread UI work.
- Hardware-backed token oracles cover resident versus SSD execution, legacy
MTP, DSpark, SSD combined with both speculative modes, directional steering,
and target-only fallback. Differential endpoint scripts cover deterministic
output, finish state, and usage when reference and Rust servers are supplied.
- GLM 5.2 has a dedicated Rust/Metal executor and DeepSeek V4 Pro uses the
generalized DeepSeek graph. Their remaining work is validation rather than
catalog plumbing: GLM MTP is still rejected, the full GLM/Pro hardware matrix
is incomplete, and Pro still needs explicit memory-admission verification.
- The remaining model-independent execution gaps are fine-grained SSD cache
telemetry, the DS4 expert-locality profiler, and resident multi-session
server batching/scheduling.
- The native UI is on Iced 0.14. Chat transcripts use its table-aware Markdown
content and viewer path, with a regression for code-styled line-count tables
produced by coding models.
## Delivery order
1. **Next:** finish the remaining DS4 execution parity: detailed SSD cache
telemetry, expert profiling, resident multi-session batching, GLM MTP, and
the GLM/Pro hardware matrix.
2. Product completion, exhaustive parity verification, and distribution.
3. Optional extensions: Dev Brain and A2UI.
## 1. Completed — tool hardening and safety
Goal: make the existing tool set safe and clear enough for productive daily
use without weakening its ability to inspect, edit, build, and test a project.
- Keep file tools strictly inside the selected project after canonicalization,
including symlinks and nonexistent write targets. Boundary escapes are
rejected, not approved as exceptions.
- Centralize tool approval. Reuse the existing visible-browser approval path
for actions that require consent instead of adding tool-specific modal
systems.
- Require confirmation before destructive shell commands or commands that
affect state outside the project, elevate privileges, launch external apps,
or create unrequested network side effects. Show the exact command and
working directory; support Allow once and Deny. Do not persist broad trust
silently.
- Define the shell environment deliberately so application credentials and
unrelated secrets are not exposed accidentally. Preserve the minimum normal
development environment needed for project commands.
- Render streamed tool calls as compact cards with their real lifecycle:
parsing, awaiting approval, queued, running, completed, failed, or stopped.
Show bounded parameters and results, provide copy/open actions where useful,
and never display raw DSML as ordinary assistant prose.
- Keep background shell jobs cancellable and observable through app stop,
session switches, compaction, and application shutdown. Retain bounded output
files only as long as needed and report cleanup failures.
- Continue enforcing context-aware read/search/bash result limits. Oversized
output must remain available through continuation or a file reference rather
than being inserted wholesale into the prompt.
- Test project-boundary and symlink escapes, destructive-command approval,
denial and cancellation, browser approval, output bounding, and session
switching with a live job.
Exit criterion: use the agent for a real inspect/edit/test cycle while every
side effect is visible, risky actions require consent, Stop works at every
stage, and no file tool can escape the selected project.
## 2. In progress — DS4 execution technology parity
Goal: finish the model-independent Metal/runtime capabilities in `ds4.c`
before adding larger model families. Every capability must be shared by local
chat and the HTTP endpoint through the single process-wide model owner.
### 2.1 Implemented — SSD streaming
Flash routed experts stream through the unchanged DS4 Metal kernels with
automatic or explicit cache budgets, cold start, generated DS4 hotlists,
preload controls, asynchronous I/O, and bounded cache eviction. Resident and
SSD generation share token oracles. Engine atomics expose resident/cache bytes,
requests, bytes read, and wait time; the UI samples and graphs their rates on
its normal metrics thread. Native cache hit/miss, eviction, and preload-progress
telemetry remains to be surfaced without changing the carried-over kernels.
SSD streaming is the capacity prerequisite for larger models and therefore
comes before GLM 5.2 and DeepSeek V4 Pro execution.
- Keep non-routed weights resident while loading routed MoE experts from the
GGUF into a bounded in-memory cache on demand.
- Port DS4's automatic cache sizing from the recommended Metal working set,
non-routed weights, KV/context requirements, graph scratch, and routed-prefill
headroom. Preserve explicit expert-count and GiB-budget modes.
- Port cold start, hot-expert preload, explicit preload count, full resident
layer policy, cache eviction, and the overlapped prefill/decode loading path.
- Keep inference correct under cache misses, short reads, cancellation, memory
pressure, unload, and model replacement. A streaming failure must produce a
bounded request error, not partial model state.
- Report resident bytes, cache capacity, hit/miss and load rates, SSD bytes and
latency, preload progress, and memory-pressure decisions in Stats.
- First prove resident-versus-streaming token parity on DeepSeek V4 Flash. Keep
the cache and I/O layer model-aware so later GLM/Pro milestones add policy and
graph support rather than a second streaming subsystem.
### 2.2 Completed for Flash — legacy MTP and DSpark
Both support GGUF formats are validated and executed by Rust. Draft generation,
confidence/margin gates, target verification, accepted-prefix commit, rollback,
strict/quality fallback, cancellation, checkpoint identity, streaming output,
usage accounting, and Stats counters use the existing generation lifecycle.
Sampled decoding remains target-only as in DS4. SSD streaming works with both
modes, and long-chat DSpark prefill has a dedicated hardware regression.
- Load and validate the optional Flash legacy-MTP or DSpark support GGUF without
treating either as a standalone model. Preserve exact support-kind and target
compatibility checks.
- Port draft generation, confidence/margin pruning, target-model verification,
accepted-prefix commit, rejected-suffix rollback/replay, cache counters, and
cancellation boundaries. The target model always owns the output stream.
- Honor MTP draft depth/margin and DSpark confidence/strict settings. Match the
reference rules for greedy, sampled, and quality modes rather than applying
speculation where DS4 falls back to target-only decode.
- Match current DS4 compatibility between SSD streaming and DSpark; remove
temporary Rust-only exclusions where the reference supports the combination.
- Expose accepted/drafted token counts, acceptance rate, verifier cost, and
effective speedup in Stats. Do not call the feature complete merely because
it produces correct tokens; it must also preserve checkpoints, tools,
streaming responses, usage accounting, and Stop behavior.
- GLM's in-model MTP path belongs to the GLM milestone, but it should reuse the
verifier/session machinery established here.
### 2.3 Mostly completed — remaining Metal execution controls
Directional steering, power throttling, prefill chunking, quality mode, weight
warming, and simulated memory pressure now affect execution. CPU helper-thread
and expert-profile settings are rejected instead of being persisted no-ops.
Porting DS4's expert-locality profile output is the remaining item here.
- Port directional steering files and exact FFN/attention application,
including DS4 defaults, validation, zero-scale behavior, and checkpoint/model
identity effects.
- Audit every accepted execution preference against the Rust executor. Power,
prefill chunking, quality/exact kernels, warm weights, simulated memory, and
expert profiling must either affect execution as DS4 defines or be disabled
with a clear explanation; persisted no-op settings are not parity.
- Add hardware-backed token/activation fixtures for each mode and keep the
ordinary resident Flash path unchanged when optional features are off.
### 2.4 Remaining — single-machine server batching
- Port DS4's resident multi-session batching and server scheduling only after
the serialized path remains the correctness oracle. Preserve per-request
cancellation, finish reasons, usage, and KV ownership while batching prefill
or decode work.
- Keep all scheduling, model state, KV state, and request handling within the
local process. Networked execution and non-Metal backends are outside the
product scope.
Exit criterion: DeepSeek V4 Flash matches DS4 token output and lifecycle in
resident, SSD-streamed, MTP, DSpark, steering, and batched-server
configurations, with optional modes off producing the same baseline behavior
as today.
## 3. Implemented executors — additional-model validation remains
GLM 5.2 has a dedicated DSA/MLA executor and DeepSeek V4 Pro uses the generalized
DeepSeek graph. Both are selectable runtimes, not catalog-only placeholders.
They are not complete parity milestones until the remaining items below pass on
the installed hardware fixtures.
### GLM 5.2
- Port the GLM DSA/MLA graph, dense-cache behavior, model-specific tensor and
quantization paths, sampling defaults, reasoning controls, prompt rendering,
and stop tokens.
- Use the already defined GLM tool syntax through the same durable local-agent
loop and expose identical behavior through every HTTP route.
- Port GLM SSD streaming policy, resident full-layer selection, and the MTP
block stored in the main GGUF. Respect GLM restrictions on power, prefill
chunking, steering, and external support models.
- Validate resident and streamed token output against DS4 fixtures before the
Model Manager advertises GLM as runnable.
### DeepSeek V4 Pro
- Generalize the Flash graph only where Pro's dimensions, layers, routed
experts, quantization layouts, or output path actually differ.
- Support resident and SSD-streamed single-machine configurations with explicit
memory admission checks. Never begin a load that cannot leave room for the
configured KV/context and graph working set.
- Match Pro prompt, sampling, checkpoint, HTTP, and agent behavior. Preserve
the reference compatibility matrix for MTP/DSpark rather than assuming Flash
support artifacts work with Pro.
- Validate supported single-file Q2/Q4 configurations against DS4 fixtures
before advertising them as runnable.
Exit criterion: each advertised model passes the same local-agent, checkpoint,
HTTP, SSD-capacity, cancellation, and deterministic token-output matrix as
DeepSeek V4 Flash.
## 4. Product completion and verification
### Reference parity and regression coverage
- Complete the automated black-box C/Rust corpus for Models, Messages, Chat
Completions, Responses, Completions, and OPTIONS in streaming and
non-streaming modes. Cover aliases/defaults, malformed input, context limits,
tools, reasoning, cache reuse, disconnects, queueing, CORS, errors, usage, and
finish reasons.
- Add every new agent/runtime/model capability to both local and endpoint
regression coverage. HTTP conversations remain client-owned and may persist
only opaque content-addressed KV entries, never application transcripts.
- Reuse DS4 prompt, sampling, KV, server, agent, and hardware fixtures. Keep
formatting, Clippy, bundle, and unit tests as commit gates; run hardware
matrices only where the required GGUF and machine are available.
### macOS product and distribution
- Finish the remaining native menu and text behavior: functional File/View/Help
actions, dynamic enabled state, undo/redo parity, and selectable/copyable
transcript text.
- Finish session/model binding and recovery surfaces needed to explain why a
checkpoint is reused or rebuilt. Keep archive as metadata-only and never
delete project contents.
- Add production signing, hardened runtime, required entitlements,
notarization, update delivery, and release automation. Preserve all upstream
DwarfStar/llama.cpp notices for adapted code and kernels.
Exit criterion: a notarized build can be installed on a clean supported Mac,
run the full local-agent and endpoint smoke matrix, restart into its previous
sessions, and update without losing projects, transcripts, models, or KV data.
## 5. Optional future extensions
These are not DS4 baseline parity and must not delay the milestones above.
### Dev Brain
- Let the user opt into one existing Obsidian vault through a native folder
picker and persist a security-scoped bookmark for sandboxed restart access.
- Add one project-independent tool with bounded search, read, and create/append
operations for Markdown notes. Return vault-relative citations and exclude
`.obsidian`, hidden files, deletion, and arbitrary overwrite.
- Reuse the existing file/search implementation with a separate explicit vault
boundary. No vault means no tool and no effect on ordinary sessions.
Exit criterion: store a memory in one project, restart, and retrieve it with a
vault-relative citation from another project without granting broader disk
access.
### A2UI local-chat surfaces
- After the ordinary tool UI is stable, use `../bDS2` as the reference for
typed cards, charts, forms, lists, metrics, mind maps, tables, and tabs.
- Validate and persist versioned structured payloads with their messages.
Render with native Iced/SVG primitives; reject model-authored HTML,
JavaScript, styles, file URLs, and arbitrary commands.
- Keep A2UI local-chat-only until a real endpoint client requires a compatible
protocol. All mutating actions continue through the normal approval path.
Exit criterion: supported surfaces render accessibly, malformed payloads fall
back safely, interactions are validated, and the same surfaces rehydrate after
restart.
+194 -54
View File
@@ -1,58 +1,187 @@
# DS4Server # DS4Server
DS4Server is a native macOS coding-agent application that rewrites the DS4Server is a native macOS application for running DwarfStar models as a local
DwarfStar (`ds4`) inference engine in Rust. It uses Rust and Iced and will combine local model loading, an coding agent and as a localhost inference server. The inference engine, model
OpenAI-compatible localhost endpoint, and project-scoped agent chat in one app. lifecycle, agent loop, and application UI are implemented in Rust with Iced;
the Metal kernels and small native integration layers are adapted from
[DwarfStar (`ds4`)](https://github.com/antirez/ds4).
DS4Server vendors and adapts the Metal kernels, Objective-C Metal glue, and ## Capabilities
visible-Chrome web tool runtime from
[DwarfStar (`ds4`)](https://github.com/antirez/ds4). Their copyright and license
notices are retained in [`native/metal/LICENSE`](native/metal/LICENSE) and
[`native/web/LICENSE`](native/web/LICENSE).
The current milestone provides a Codex-inspired project/session layout. A native - **Project-based coding chat.** Add local project folders, keep multiple
macOS folder picker selects each workspace, then the app asks for its display persistent sessions per project, and rename, retitle, pin, archive, compact,
name. Projects and sessions are persisted through Diesel in SQLite, while rebuild, or delete sessions. Chats stream Markdown, reasoning, tool activity,
settings live in a YAML file beside it that lists only the values you changed context use, and generation speed.
and can be edited by hand. Open Preferences with `Command-,` to configure model, - **Local coding tools.** The agent can read, search, list, write, and edit
generation, runtime, local endpoint, and idle-unload settings. The separate Model Manager project files; run and monitor asynchronous shell commands; search Google;
(`Shift-Command-M`) and visit rendered web pages. File access is confined to the project, output
lists local main and DSpark artifacts, their on-disk sizes and state, and lets is bounded, risky shell or visible-browser actions require approval, and
you download, resume, validate, or delete them. Rust-native background work tool calls are validated against the same typed schemas shown to the model.
shows live byte progress, speed, and ETA in Model Manager and the app status - **Fresh-agent Ralph loops.** For a bounded coding objective, the agent can
bar. Stopping or quitting keeps the partial file; the next Download/Resume run foreground rounds in fresh model contexts while preserving workspace
action continues from that exact byte after relaunch. Exact size and SHA-256 changes between rounds. A validated structured report is the only
verification happen before an artifact becomes usable. conversational handoff to the next worker; the loop stops on reported
completion, a blocker, failure, cancellation, or its configurable round
limit.
- **Workspace instructions and reusable skills.** A global `AGENTS.md` plus
project and nested `AGENTS.md`/`AGENTS.local.md` files provide hierarchical
guidance that is reconciled as the agent enters or changes a workspace area.
Standard `~/.agents/skills/<name>/SKILL.md` packages are advertised by
metadata and loaded in full only when a task matches.
- **Project-backed Dev Brain.** An optional managed Obsidian vault provides
source-verified wiki pages, ranked search, and on-demand skills. Each session
receives the verified skill names, descriptions, and paths, then loads full
instructions only when a task matches.
- **Long-running sessions.** Transcripts and summaries are stored in SQLite.
Automatic and manual context compaction preserve the complete visible chat,
while durable KV checkpoints make follow-up turns and relaunches resumable.
- **Native Metal inference.** DeepSeek V4 Flash, DeepSeek V4 Pro, and GLM 5.2
run through model-specific Rust/Metal executors with long-context prefill,
sampling, cancellation, idle unload, and a bounded resident session pool.
Supported acceleration includes SSD expert streaming, DeepSeek legacy MTP,
DSpark, GLM MTP, directional steering, and model-specific memory admission.
- **Local HTTP APIs.** A configurable listener on `127.0.0.1:4000` exposes
Models, Chat Completions, Completions, Anthropic Messages, and Responses APIs
with streaming, tool calls, usage accounting, cancellation, and prefix-aware
KV reuse. Endpoint conversations remain client-owned and are not added to
project history.
- **Model and runtime management.** Model Manager downloads, resumes, verifies,
and deletes supported GGUF artifacts with byte progress, speed, and ETA.
Preferences cover generation, runtime, SSD, speculative decoding, steering,
KV storage, the local endpoint, and idle unload. The Stats view reports model,
cache, SSD, speculative, session, and HTTP activity.
The selected DeepSeek V4 Flash model can run directly from a project session. Projects, sessions, and transcripts use SQLite. Settings use a compact YAML file
The model, KV/compressor state, 43-layer graph, sampling, and lifecycle are that contains only values changed from their defaults and can also be edited by
owned by Rust; a fixed snapshot of the Objective-C Metal boundary and unchanged hand.
Metal kernels is vendored and built inside this repository. Tokens stream into the chat UI, Stop cancels generation,
follow-up turns reuse durable transcript and KV state, and the model unloads
after the idle timeout. The graph uses the full configured context with the
ratio-4 sparse indexer.
Project chat includes the native `ds4_agent.c` starting tool set: bounded file ## A2UI widgets
read/continuation, write, anchored edit, search, directory listing, asynchronous
shell jobs, Google search, and rendered page visits. Tool calls and results are
persisted as transcript roles and automatically continue the same model turn.
File tools stay inside the selected project. Web tools ask before starting a
visible Chrome profile.
Long sessions compact automatically while retaining the complete scrollable DS4Server renders A2UI v1.0 surfaces with these widgets:
chat. Each compaction appears in history with its durable summary, and a missing
or incompatible KV checkpoint rebuilds from the latest summary and subsequent
chat. An idle session's `…` menu can force compaction after new chat has been
added since the latest marker, or discard its checkpoint for a clean rebuild.
The app also listens on `127.0.0.1:4000` by default for Models, Chat - **Content:** Markdown text, images (including avatars), icons, video and audio
Completions, Completions, Anthropic Messages, and Responses APIs. The listener, players, and dividers.
port, and opt-in CORS are configurable in Preferences. The - **Layout:** rows, columns, lists, cards, modals, and tabs.
endpoint and local chat share the single model owner. External conversations - **Controls:** buttons, text fields, checkboxes, sliders, date/time inputs, and
are client-managed and never enter the project, session, message, or transcript choice pickers.
database; only opaque content-addressed KV cache files are retained. Model - **Research:** bar, line, area, stacked-bar, pie, donut, and heatmap charts; tables,
discovery advertises only supported main artifacts that are fully downloaded metrics, timelines, maps, mind maps, and forms.
and verified on disk.
Video posters render in the surface; video and audio playback uses native
macOS AVKit player windows with standard transport, volume, and fullscreen
controls.
The newest A2UI surface is pinned above the transcript in a default 50/50
workspace split. Dismissing it requires confirmation and records a durable
boundary in the chat database, so reopening the session restores only the
newest surface created after the last dismissal. Left and right arrow buttons
reconstruct earlier and later read-only surface epochs from the recorded A2UI
protocol calls; model updates remain routed only to the current surface.
### Live A2UI validation
With DS4Server running and its local endpoint enabled, run the opt-in model
validation suite with:
```sh
cargo run --release -- validate-a2ui
```
It sends natural-language chart, form, choice, and full-composition prompts
through the current A2UI tool description, then replays every returned protocol
message through the real catalog and component-graph validators. Use `--case
pie-natural`, `--case composed-basics`, `--attempts 3`, `--model MODEL_ID`, or
`--endpoint URL` to narrow or repeat runs. This slow, model-dependent suite is
intentionally separate from `cargo test`.
### Headless model evaluation
Run a bounded, instrumented generation through the production runtime without
opening the UI:
```sh
cargo run --release -- model-eval \
--model qwen3.8-flash-next \
--reasoning low \
--acceleration off \
--power-percent 100
```
The evaluator first performs a short unmeasured warmup through the same system-prompt
builder and production runtime as the UI, then resets its GPU counters and starts
the measured request. `warmup_result.elapsed_ms` includes initial model loading;
its `timing` object separates `model_load_ms` and `non_loading_elapsed_ms` (the
latter still includes setup, inference, and checkpoint work). Do not add the
reported load time to the warmup total. A `model_loaded` event reports the same
load duration as the shared runtime metrics. Canary samples retain their start
`phase` and record `completion_phase`, actual completion `elapsed_ms`, and host
observation `observed_elapsed_ms`; per-phase summaries group by probe start.
`result.timing.decode_ms` measures the shared engine decode loop, including
sampling and streamed output callbacks but excluding subsequent pending-token
finalization and checkpoint work. Stats shows the same measurement as "Decode
loop (without checkpoint)". The older host-observed interval through request
completion is retained as `decode_to_request_end_ms`; it is not pure decode time.
`decode_loop_tokens_per_second` uses completion tokens divided by the loop duration;
`decode_tokens_per_second` remains the UI's last progress rate. `checkpoint_io`
reports request operation counts and the last read/write duration, not summed I/O
time. A missing decode duration is `null`, not zero.
The default test
prompt identifies a prose-generation test and asks the model to choose
its own genre, setting, and characters, then output only a complete short story
without questions, choices, limitations, or coding discussion. JSON Lines report
runtime phases, prefill and decode speed, Metal busy time,
command-buffer duration, CPU, physical memory, disk I/O, and the complete reasoning
and answer. `--input-file PATH` appends a UTF-8 file to the supplied prompt for
realistic long-prefill tests. Repeat `--prompt` to run multiple turns in one
ongoing chat: every later turn includes all preceding user prompts and complete
assistant responses and continues from the same session checkpoint. An independent
Metal canary runs inside the same
process as the production runtime through
loading, prefill, decode, and finishing, reporting per-phase scheduling and
completion latency plus stalls that could make a UI unresponsive. Use
`--acceleration on|off` to compare DSpark on DeepSeek or integrated MTP on
GLM/Qwen. The canary itself submits GPU work, so use `--canary off` for clean
throughput measurements and `--canary on` for responsiveness diagnostics. The UI
canary is off by default and can be enabled in Stats. A supervisor terminates
the worker only when startup or token progress stalls, or when the memory limit
is exceeded; there is no whole-run timeout. See `model-eval --help` for all
overrides. The evaluator only opens already installed model artifacts and never
downloads missing models.
Evaluation defaults to `--plain-chat on`: the shared UI prompt builder omits
agent/tool schemas, skill instructions and the built-in coding-agent system
prompt; an explicitly customized system prompt is preserved. The corresponding
UI configuration is `plain_chat: true` in its YAML configuration. In that mode
the UI also skips workspace/date injections, extension hooks, A2UI handling,
tool execution, automatic response retries and automatic first-turn titles.
Use a **new chat** in both frontends: existing history is never silently removed.
The normal UI default remains agent mode (`plain_chat: false`). `--plain-chat off`
restores agent prompt construction for diagnostics, but does not make the harness
an agent/tool executor. The effective mode and complete system prompt are reported
in the start receipt; do not compare runs with different modes.
For a summary, story, and Python one-shot in a single chat, pass three prompts:
```sh
target/release/ds4-server model-eval \
--model qwen3.8-flash-next --reasoning low \
--power-percent 100 --acceleration off --canary on \
--input-file README.md \
--prompt 'Give a concise summary of the following project README. Return the summary directly without asking questions.' \
--prompt 'This is a prose-generation test. Respond only with a complete short fictional story in ordinary prose. Choose the genre, setting, and characters yourself and begin the story immediately. Do not ask questions, offer choices, explain limitations, or discuss coding.' \
--prompt 'This is an offline code-generation benchmark. Do not invoke tools or emit tool-call markup. Return exactly one Python code block defining has_close_elements(numbers: list[float], threshold: float) -> bool. It must return True if any two distinct numbers are closer than threshold. Include no explanation outside the code block.'
```
The input file is appended only to the first prompt. Each turn starts after the
previous response finishes; `measurement_start` records the turn number and the
number of history messages (1, 3, 5), and each `result` reports its own timings,
cached tokens, reasoning, and answer. The warmup uses a separate session and is
not included in this history.
## Build and run
DS4Server targets Apple silicon with macOS 15 or newer and requires Rust 1.97 plus
`cargo-packager` 0.11.8.
```sh ```sh
cargo install cargo-packager --locked --version 0.11.8 cargo install cargo-packager --locked --version 0.11.8
@@ -60,14 +189,25 @@ make bundle
open target/release/DS4Server.app open target/release/DS4Server.app
``` ```
State is stored at: Tagged versions are also published as Apple Silicon DMG files on the
[repository releases page](https://git.rfc1437.de/hugo/DS4Server/releases).
## Application data
```text ```text
~/Library/Application Support/DS4Server.rfc1437.de/data.sqlite3 ~/Library/Application Support/de.rfc1437.ds4server/data.sqlite3
~/Library/Application Support/DS4Server.rfc1437.de/config.yaml ~/Library/Application Support/de.rfc1437.ds4server/config.yaml
~/Library/Application Support/DS4Server.rfc1437.de/kv-cache/ ~/Library/Application Support/de.rfc1437.ds4server/AGENTS.md
~/Library/Application Support/de.rfc1437.ds4server/models/
~/Library/Application Support/de.rfc1437.ds4server/kv-cache/
``` ```
Deleting a project or session removes only DS4Server metadata. It never deletes Deleting a project or session removes DS4Server metadata and its associated
the referenced project directory. See [PLAN.md](PLAN.md) for the implementation checkpoint; it never deletes the referenced project directory. Standard agent
roadmap. skills live separately under `~/.agents/skills/`. See the
[issue tracker](https://git.rfc1437.de/hugo/DS4Server/issues) for remaining
implementation work.
The adapted Metal and browser sources retain their upstream notices in
[`native/metal/LICENSE`](native/metal/LICENSE) and
[`native/web/LICENSE`](native/web/LICENSE).
+144
View File
@@ -0,0 +1,144 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ds4server.local/a2ui/v1_0/catalog.json",
"title": "DS4Server A2UI Catalog",
"catalogId": "https://ds4server.local/a2ui/v1_0/catalog.json",
"instructions": "Use the Basic Catalog for interaction and layout. Use research components for compact analytical results.",
"components": {
"Text": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Text" },
"Image": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Image" },
"Icon": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Icon" },
"Video": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Video" },
"AudioPlayer": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/AudioPlayer" },
"Divider": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Divider" },
"Row": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Row" },
"Column": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Column" },
"List": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/List" },
"Card": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Card" },
"Modal": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Modal" },
"Tabs": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Tabs" },
"Button": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Button" },
"TextField": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/TextField" },
"CheckBox": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/CheckBox" },
"Slider": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/Slider" },
"DateTimeInput": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/DateTimeInput" },
"ChoicePicker": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/components/ChoicePicker" },
"Chart": { "$ref": "#/$defs/chart" },
"Table": { "$ref": "#/$defs/table" },
"Metric": { "$ref": "#/$defs/metric" },
"Timeline": { "$ref": "#/$defs/timeline" },
"Map": { "$ref": "#/$defs/map" },
"MindMap": { "$ref": "#/$defs/mindMap" },
"Form": { "$ref": "#/$defs/form" }
},
"functions": {
"required": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/required" },
"regex": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/regex" },
"length": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/length" },
"numeric": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/numeric" },
"email": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/email" },
"formatString": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/formatString" },
"formatNumber": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/formatNumber" },
"formatCurrency": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/formatCurrency" },
"formatDate": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/formatDate" },
"pluralize": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/pluralize" },
"openUrl": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/openUrl" },
"and": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/and" },
"or": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/or" },
"not": { "$ref": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json#/functions/not" }
},
"$defs": {
"surfaceProperties": {
"type": "object",
"properties": {
"iconUrl": { "type": "string", "format": "uri" },
"agentDisplayName": { "type": "string" }
},
"additionalProperties": true
},
"researchBase": {
"type": "object",
"properties": {
"id": { "type": "string" },
"component": { "type": "string" },
"title": { "type": "string" },
"accessibility": { "type": "object" },
"weight": { "type": "number" },
"checks": { "type": "array" }
},
"required": ["id", "component"]
},
"chart": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "Chart" }, "chartType": { "enum": ["bar", "line", "area", "stackedBar", "pie", "donut", "heatmap"] }, "series": {} }, "required": ["chartType", "series"] }
],
"unevaluatedProperties": false
},
"table": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "Table" }, "columns": {}, "rows": {} }, "required": ["columns", "rows"] }
],
"unevaluatedProperties": false
},
"metric": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "Metric" }, "label": {}, "value": {}, "detail": {}, "trend": {} }, "required": ["label", "value"] }
],
"unevaluatedProperties": false
},
"timeline": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "Timeline" }, "events": {} }, "required": ["events"] }
],
"unevaluatedProperties": false
},
"map": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "Map" }, "locations": {} }, "required": ["locations"] }
],
"unevaluatedProperties": false
},
"mindMap": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "MindMap" }, "nodes": {} }, "required": ["nodes"] }
],
"unevaluatedProperties": false
},
"form": {
"allOf": [
{ "$ref": "#/$defs/researchBase" },
{ "properties": { "component": { "const": "Form" }, "children": { "$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList" }, "submitLabel": { "type": "string" }, "action": { "$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/Action" } }, "required": ["children"] }
],
"unevaluatedProperties": false
},
"anyComponent": {
"oneOf": [
{ "$ref": "#/components/Text" }, { "$ref": "#/components/Image" }, { "$ref": "#/components/Icon" },
{ "$ref": "#/components/Video" }, { "$ref": "#/components/AudioPlayer" }, { "$ref": "#/components/Divider" },
{ "$ref": "#/components/Row" }, { "$ref": "#/components/Column" }, { "$ref": "#/components/List" },
{ "$ref": "#/components/Card" }, { "$ref": "#/components/Modal" }, { "$ref": "#/components/Tabs" },
{ "$ref": "#/components/Button" }, { "$ref": "#/components/TextField" }, { "$ref": "#/components/CheckBox" },
{ "$ref": "#/components/Slider" }, { "$ref": "#/components/DateTimeInput" }, { "$ref": "#/components/ChoicePicker" },
{ "$ref": "#/components/Chart" }, { "$ref": "#/components/Table" }, { "$ref": "#/components/Metric" },
{ "$ref": "#/components/Timeline" }, { "$ref": "#/components/Map" }, { "$ref": "#/components/MindMap" },
{ "$ref": "#/components/Form" }
],
"discriminator": { "propertyName": "component" }
},
"anyFunction": {
"oneOf": [
{ "$ref": "#/functions/required" }, { "$ref": "#/functions/regex" }, { "$ref": "#/functions/length" },
{ "$ref": "#/functions/numeric" }, { "$ref": "#/functions/email" }, { "$ref": "#/functions/formatString" },
{ "$ref": "#/functions/formatNumber" }, { "$ref": "#/functions/formatCurrency" }, { "$ref": "#/functions/formatDate" },
{ "$ref": "#/functions/pluralize" }, { "$ref": "#/functions/openUrl" }, { "$ref": "#/functions/and" },
{ "$ref": "#/functions/or" }, { "$ref": "#/functions/not" }
]
}
}
}
+15
View File
@@ -0,0 +1,15 @@
# Dev Brain purpose
Dev Brain compiles durable, source-backed knowledge from the registered projects.
## Priorities
- Architecture, behavior, decisions, invariants, workflows, and relationships.
- Small topic pages that answer recurring development questions.
- Visible uncertainty and exact project provenance.
## Recurring questions
- Where does a behavior live, and what must remain invariant when it changes?
- Which decisions constrain the current implementation?
- Which project sources must be rechecked before relying on this page?
+67
View File
@@ -0,0 +1,67 @@
# Dev Brain schema
The registered projects are authoritative. This vault is a derived, human-readable wiki.
## Managed paths
DS4Server manages `purpose.md`, `schema.md`, `index.md`, `skills.md`, `log.md`, and topic pages below `projects/`, `subsystems/`, `concepts/`, `decisions/`, `invariants/`, `workflows/`, and `skills/`. Topic pages must opt in with `dev_brain: true`. Other notes, hidden files, Obsidian settings, attachments, and trash are never modified.
## Topic frontmatter
```yaml
---
dev_brain: true
type: subsystem # project, subsystem, concept, decision, invariant, workflow, or skill
project: Registered project name
status: verified # verified, stale, or needs-review
verified_at: 2026-07-27T12:00:00Z
sources:
- project: Registered project name
path: src/example.rs
symbol: optional_symbol
revision: latest-commit-containing-this-file-content
# For a clean tracked file, use `git log -1 --format=%H -- src/example.rs`,
# or a unique lowercase hex prefix of at least 7 characters. Use hash when
# this file differs from HEAD, is untracked, or the registered project is not Git.
---
```
`project` names a registered project, not this Dev Brain vault; `dev_brain_info` lists the exact registered names and folders. Each source `path` is relative to that registered project's folder; never resolve it inside the vault. Cite only files that support the page's claims, not every dirty file in the project. Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. A revision is the newest commit that changed that source file, returned by `git log -1 --format=%H -- path`; validation requires that per-file revision and compares its file content with the current clean file. Every source therefore has its own revision; never stamp all sources with the repository's overall HEAD. Use a hash when that specific file differs from HEAD, is untracked, or its registered project is not Git.
## Skills
Skills are on-demand instructions stored as one Markdown file per skill below `skills/`. They use the same provenance and status fields as topic pages, set `type: skill`, and additionally require `name` and `description` frontmatter. Names contain 164 lowercase letters, digits, or hyphens, with no leading, trailing, or consecutive hyphens. Descriptions contain 11024 characters and explain both what the skill does and when to use it. Only skills whose status and current evidence are both `verified` appear in generated `skills.md` and the session system prompt. The prompt exposes each verified skill's name, description, and Markdown path; read the complete matching file on demand before following it. `index.md` lists all managed topic pages, while `skills.md` lists verified skills only. Do not hand-edit either generated index.
Files with `built_in: true` are app-managed skills restored from the DS4Server bundle. Do not create or edit them by hand.
```yaml
---
dev_brain: true
type: skill
name: review-release
description: Verify a release candidate against the project checklist. Use before publishing a release.
project: Registered project name
status: verified
verified_at: 2026-07-27T12:00:00Z
sources:
- project: Registered project name
path: docs/releasing.md
revision: full-or-unique-short-clean-git-revision
---
```
## Compilation
Read the purpose first. Examine high-signal manifests, documentation, schemas, entry points, public interfaces, and tests. Create the smallest coherent topic set that answers the purpose; do not mirror every source file. Use ordinary Obsidian wikilinks and embeds to connect topics.
## Querying
Start with `index.md` or ranked search, then follow links and backlinks. A `verified` page is authoritative only while all recorded evidence is current. Treat `stale` pages as navigation leads and `needs-review` pages as explicit uncertainty; check project sources before making claims from either.
## Refresh and semantic validation
When validation reports drifted sources, handle each listed file independently: re-read it, compare its current behavior with the page's documented findings, update, split, merge, or retire affected knowledge, then update only that source's revision or hash. For a large revision-backed file, `git diff <recorded-revision> -- path` shows the change from the recorded evidence to the current working-tree version; use it to focus the review, then read enough current context to validate the affected findings. Inspect the commits affecting that path since the recorded revision as well. When code was removed, inspect the full commits and search the current project, callers, and tests before concluding the behavior disappeared: it may have been renamed, replaced, or moved into another file that should replace or join the page's cited sources. Repeat for every listed file and re-run validation. Keep `index.md` exact, and append a material update entry to `log.md` with source revisions. Only mark a page `verified` after its claims have been checked against its current evidence. Unsupported conclusions stay `needs-review`.
## Publication
Call `dev_brain_info`, then maintain these pages with the ordinary file tools. Append material updates to `log.md`; `index.md` and `skills.md` are generated and should not be hand-maintained. Finish with `dev_brain_validate`. Structural or provenance errors must be repaired, while broken links are warnings so valid work remains editable instead of being discarded.
@@ -0,0 +1,58 @@
---
dev_brain: true
type: skill
built_in: true
name: create-dev-brain-skill
description: Create or update a source-backed skill in Dev Brain. Use when adding reusable instructions that should activate on demand in future development tasks.
project: DS4Server built-in
status: verified
verified_at: 2026-07-30T00:00:00Z
sources: []
---
# Create a Dev Brain skill
Create a skill when a repeatable development task needs instructions beyond the model's general knowledge. Keep it concise and source-backed.
## File structure
A Dev Brain skill is one Markdown file, not a directory:
```text
skills/
└── skill-name.md
```
Use this structure:
```yaml
---
dev_brain: true
type: skill
name: skill-name
description: State what the skill does and when to use it.
project: Exact registered project name
status: needs-review # change to verified only after checking every claim
verified_at: 2026-07-30T12:00:00Z
sources:
- project: Exact registered project name
path: path/to/authoritative-source.md
revision: latest-commit-that-changed-this-file
---
# Human-readable skill title
Write direct instructions for completing the task. Link to related Dev Brain pages when useful.
```
Use lowercase letters, digits, and hyphens for `name`. Put all trigger conditions in `description`. Cite at least one registered project source, using the exact per-file revision for a clean Git file or its lowercase SHA-256 hash when required by `schema.md`.
## Create and activate
1. Call `dev_brain_info` and read `purpose.md` and `schema.md`.
2. Read the authoritative project sources for the instructions.
3. Write `skills/<name>.md` with current evidence and `status: verified`; use `needs-review` until every claim is checked.
4. Append the material change to `log.md`.
5. Run `dev_brain_validate`.
Validation is the activation step. It verifies the file, rebuilds generated `skills.md`, and makes the skill's name, description, and path available to the agent. Never edit `skills.md` directly.
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="#fff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M11.75 4.5 6.25 10l5.5 5.5"/>
</svg>

After

Width:  |  Height:  |  Size: 201 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="#fff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="m8.25 4.5 5.5 5.5-5.5 5.5"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M2.75 6V5.75A1.75 1.75 0 0 1 4.5 4h3l1.7 1.75h6.3A1.75 1.75 0 0 1 17.25 7.5v.5"/>
<path d="M3.75 8h13.5a1 1 0 0 1 .95 1.3l-1.65 5.1A2.25 2.25 0 0 1 14.4 16H5.25a2.25 2.25 0 0 1-2.2-1.8L2 9.75A1.43 1.43 0 0 1 3.4 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 391 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 2.5v2M7.5 2.5h5M4 7.5A2.5 2.5 0 0 1 6.5 5h7A2.5 2.5 0 0 1 16 7.5v6a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 4 13.5zM4 10H2.5M17.5 10H16M7.5 9.5h.01M12.5 9.5h.01M7.5 13h5"/>
</svg>

After

Width:  |  Height:  |  Size: 346 B

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
{
"format": 1,
"source": {
"repository": "Youssofal/Qwen3.8-Flash-Next-MTPLX-Bare-Speed",
"url": "https://huggingface.co/Youssofal/Qwen3.8-Flash-Next-MTPLX-Bare-Speed/tree/74559cdf34fbfc0b593de72d17e93f37fd4f9ea7",
"revision": "74559cdf34fbfc0b593de72d17e93f37fd4f9ea7",
"base_repository": "Qwen/Qwen3.8-Flash-Next",
"base_url": "https://huggingface.co/Qwen/Qwen3.8-Flash-Next/tree/de4b8e4d43b917e7706784d8bb445c9af86a3540",
"base_revision": "de4b8e4d43b917e7706784d8bb445c9af86a3540",
"license": "Qwen Community License 1.0",
"converter": "qwen38-artifacts-v1-identity"
},
"tensor_inventory": "qwen38-flash-next-bare-speed-tensors.tsv",
"tensor_inventory_sha256": "b5731e6febcf865d276a0e7b144da02375f2d7e3129594f271ee9c6c351f4c8f",
"config": {
"/architectures/0": "Qwen4ExpForConditionalGeneration",
"/model_type": "qwen4_exp",
"/quantization_config/bits": 4,
"/quantization_config/group_size": 64,
"/text_config/full_attention_interval": 4,
"/text_config/hc_count": 4,
"/text_config/hc_lowrank": 320,
"/text_config/hidden_size": 2560,
"/text_config/indexer_budget": 2048,
"/text_config/indexer_compress_ratio": 4,
"/text_config/indexer_head_dim": 128,
"/text_config/indexer_kv_heads": 1,
"/text_config/indexer_n_heads": 4,
"/text_config/linear_conv_kernel_dim": 4,
"/text_config/linear_key_head_dim": 128,
"/text_config/linear_num_key_heads": 16,
"/text_config/linear_num_value_heads": 48,
"/text_config/linear_value_head_dim": 128,
"/text_config/max_position_embeddings": 262144,
"/text_config/moe_intermediate_size": 640,
"/text_config/mtp_num_hidden_layers": 1,
"/text_config/ngram_size": 3,
"/text_config/ngram_vocab_size_base": 20000000,
"/text_config/num_attention_heads": 24,
"/text_config/num_experts": 512,
"/text_config/num_experts_per_tok": 10,
"/text_config/num_hidden_layers": 48,
"/text_config/num_key_value_heads": 2,
"/text_config/output_gate_type": "sigmoid",
"/text_config/partial_rotary_factor": 0.25,
"/text_config/ple_conv_kernel_size": 4,
"/text_config/ple_embed_dim": 2560,
"/text_config/rms_norm_eps": 0.000001,
"/text_config/rope_parameters/rope_theta": 10000000,
"/text_config/shared_expert_intermediate_size": 640,
"/text_config/split_ngram_parts": 128,
"/text_config/vocab_size": 248320
},
"runtime": {
"/arch_id": "qwen4-next",
"/mtp_contract/base_hidden_variant": "post_norm",
"/mtp_contract/concat_order": "embedding_hidden",
"/mtp_contract/hidden_variant": "post_norm",
"/mtp_contract/mtp_position_mode": "cache",
"/mtp_contract/mtp_quant_group_size": 64,
"/mtp_contract/mtp_quant_mode": "affine",
"/mtp_depth_max": 3,
"/mtp_sidecar": "prequantized-mlx-affine-qwen-moe-switch-mlx",
"/mtplx_version": "2.9.3",
"/public_model_id": "mtplx-flash-next-bare-speed",
"/sampler/temperature": 1.0,
"/sampler/top_k": 20,
"/sampler/top_p": 0.95
},
"representative_rows": [
{"class":"expert","file":"model-00001-of-00017.safetensors","tensor":"language_model.model.layers.0.mlp.switch_mlp.gate_proj.weight","row":0,"values":64,"bits":4,"group_size":64,"sha256":"0fe4ae597854a1483e2d63f07cada72d4656c4178e1d696ce4311b838ba05964"},
{"class":"dense","file":"model-00016-of-00017.safetensors","tensor":"language_model.model.layers.0.linear_attn.in_proj_qkv.weight","row":0,"values":64,"bits":4,"group_size":64,"sha256":"59d7d7bf84e88823704d9f7b2c84e41b46f481c658ed8a485f2fd063ecf9ca34"},
{"class":"qsa","file":"model-00016-of-00017.safetensors","tensor":"language_model.model.layers.11.self_attn.indexer.index_qk_proj.weight","row":0,"values":64,"bits":8,"group_size":64,"sha256":"c803ea77148621a5d6dfa4060a52c75f57185234a0bec4771671f69336bb8346"},
{"class":"gdn","file":"model-00016-of-00017.safetensors","tensor":"language_model.model.layers.0.linear_attn.A_log","row":0,"values":48,"sha256":"88c53a2a04bda1d96ee1ade6fa7dfa9c49d3245b1cc1e83673f02b7a36e07a85"},
{"class":"ple","file":"ngram-table.safetensors","tensor":"ngram.weight","row":0,"values":64,"bits":4,"group_size":32,"sha256":"2243dd9766046bb80d98e3baf5e59ba246958cfe4f340ebce8c1fff57a2810d9"},
{"class":"mtp","file":"mtp.safetensors","tensor":"mtp.layers.0.mlp.switch_mlp.gate_proj.weight","row":0,"values":64,"bits":4,"group_size":32,"sha256":"393d82675ae7e25275243fd4907d3894146ac1c5662b601aae998c489d10af5c"}
],
"files": [
{"path":"model-00001-of-00017.safetensors","role":"core","size":4666167150,"sha256":"a27232c9434b9d8961f198cf36f44e228b3f23a5f8d874ba4ca8af32a1b23ffc"},
{"path":"model-00002-of-00017.safetensors","role":"core","size":4299165712,"sha256":"6268ae80950f6b935fca672fae5aa455eb3ea4f139730e54e6e5f8581faa94dc"},
{"path":"model-00003-of-00017.safetensors","role":"core","size":4666167168,"sha256":"71a9abbd420dce567f2c06593bfee2d6cea681989d51eaef1b1c5dffd65a844b"},
{"path":"model-00004-of-00017.safetensors","role":"core","size":4299165702,"sha256":"c8112da735bb1c8dbc4757926ab8ec1eba7a707a8767c7e43c6150586cf63a4c"},
{"path":"model-00005-of-00017.safetensors","role":"core","size":4666167169,"sha256":"48258b931e7596c1b43c8b38c044060787ef6d85cca2c1935852bc962801f792"},
{"path":"model-00006-of-00017.safetensors","role":"core","size":4299165708,"sha256":"f357d2d66f7b768ebb19ba464556bbe46c572cf3cd1f8226faddf9216b150d0c"},
{"path":"model-00007-of-00017.safetensors","role":"core","size":4666167168,"sha256":"d3152e808d013e3afa4a9c391f6d8088e74a10303c89d45fb61dad7e457160f6"},
{"path":"model-00008-of-00017.safetensors","role":"core","size":4299165697,"sha256":"1d93bd4872f338fa19416bac64ebeb05aedf2721a07ac48e37c53921252c95c1"},
{"path":"model-00009-of-00017.safetensors","role":"core","size":4666167162,"sha256":"d22d4be580296e3d4fa5f4fe5902c8fc448d8204b6dcde4641f87b9c675930ca"},
{"path":"model-00010-of-00017.safetensors","role":"core","size":4299165708,"sha256":"111d0166c888cabb7d6fc19c4bb7eb42e2b51946f71b4983dd750b031755637e"},
{"path":"model-00011-of-00017.safetensors","role":"core","size":4666167159,"sha256":"73dad882cf94a5486cfe0dcda876b0f740556bc469608d2ced7a07540e699803"},
{"path":"model-00012-of-00017.safetensors","role":"core","size":4299165706,"sha256":"a6057d5a06276461760a4f4e88991550a43abdc5f8afacef06e860f5e7843393"},
{"path":"model-00013-of-00017.safetensors","role":"core","size":4666167174,"sha256":"ff25dc6e1c250c9b9ece83d59111a4c4055dad1b1f160546c614dcc17395b152"},
{"path":"model-00014-of-00017.safetensors","role":"core","size":4299165704,"sha256":"d8c9de3ae28abccc8e3da09dc92fbbed4039d6d65dabb2657edebc11b2b09652"},
{"path":"model-00015-of-00017.safetensors","role":"core","size":4666167146,"sha256":"a5438d468074c709febdeadb9f6a2db54fa9fb30a37c33de78584f28d6fa1191"},
{"path":"model-00016-of-00017.safetensors","role":"core","size":4300831084,"sha256":"4a07409bd568cd1435641495d90c805dcb0d0e5d8dc64409273d604a7d06b342"},
{"path":"model-00017-of-00017.safetensors","role":"core","size":18354282,"sha256":"9c064e0b8dc0054e2b453fe2b8450c287e1ede6e74bb74ecf5aaed1ef9271913"},
{"path":"ngram-table.safetensors","role":"ple","size":32000154008,"sha256":"af7f0880c1798e60353b07cc6b0faa0d41f78734818d858ff013ab7e3bce9de5"},
{"path":"mtp.safetensors","role":"mtp","size":1672575532,"sha256":"1bd4b2fc20a93b53f22b4cab0b14d68bc0fe666bb64e4e2f898872c30d7d26ae"},
{"path":"model.safetensors.index.json","role":"core-index","size":284868,"sha256":"81e77f5b25bb29e25e239ae2f9f324c9cada379ee600441d6c229f1d95a13443"},
{"path":"config.json","role":"config","size":227092,"sha256":"826302af9ff3293cfc9975c9a5a96546fa0aa9f0808505adfc30a62fef806965"},
{"path":"generation_config.json","role":"config","size":202,"sha256":"e70c136c1b78ddc1fb0905bac8e733a4dc448d4f852a5dd75143fffc70be550e"},
{"path":"mtplx_runtime.json","role":"config","size":2676,"sha256":"381c7666872f878542494a48a874bb0784c72a955d18cc077324d3c9e74b8cdb"},
{"path":"chat_template.jinja","role":"tokenizer","size":8952,"sha256":"c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041"},
{"path":"tokenizer.json","role":"tokenizer","size":12809320,"sha256":"0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3"},
{"path":"tokenizer_config.json","role":"tokenizer","size":17928,"sha256":"b11349aafa7cdc6a320767cf7ceb29ed82f7eda5d65e8e0819e76f0ce947bf27"},
{"path":"vocab.json","role":"tokenizer","size":6722759,"sha256":"ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003"},
{"path":"merges.txt","role":"tokenizer","size":3353259,"sha256":"a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d"},
{"path":"LICENSE","role":"license","size":3235,"sha256":"a0dc422560841fd68e06d974907f8b4c709bca44a67daad2b528437bdf676c08"}
],
"excluded": [
{"path":"model-vision.safetensors","reason":"Vision tensors are outside the DS4Server text artifact.","size":897899145,"sha256":"5aff6983f36f52acbecc1f61f703ae7b927e74525bf3b1133ba276bb7a5b52b1"},
{"path":"preprocessor_config.json","reason":"Vision preprocessing is outside the DS4Server text artifact.","size":390,"sha256":"27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516"},
{"path":"processor_config.json","reason":"Vision preprocessing is outside the DS4Server text artifact.","size":367,"sha256":"dcc7ce0121d6d1c514804e68e5673e04c239ad2547339ea6330289d272a748f8"},
{"path":"video_preprocessor_config.json","reason":"Video preprocessing is outside the DS4Server text artifact.","size":385,"sha256":"7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13"}
]
}
+10 -5
View File
@@ -10,15 +10,20 @@ fn main() {
cc::Build::new() cc::Build::new()
.include("native/metal") .include("native/metal")
.file(metal) .file(metal)
.file("native/metal/ds4_canary.m")
.flag("-fobjc-arc") .flag("-fobjc-arc")
.flag("-ffast-math")
.flag("-mcpu=native")
.opt_level(3) .opt_level(3)
.compile("ds4_metal"); .compile("ds4_metal");
println!("cargo:rerun-if-changed=native/web"); println!("cargo:rerun-if-changed=native/media");
cc::Build::new() cc::Build::new()
.include("native/web") .file("native/media/ds4_media.m")
.file("native/web/ds4_web.c") .flag("-fobjc-arc")
.opt_level(2) .compile("ds4_media");
.compile("ds4_web");
println!("cargo:rustc-link-lib=framework=Foundation"); println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=AppKit");
println!("cargo:rustc-link-lib=framework=AVFoundation");
println!("cargo:rustc-link-lib=framework=AVKit");
println!("cargo:rustc-link-lib=framework=Metal"); println!("cargo:rustc-link-lib=framework=Metal");
} }
+184
View File
@@ -0,0 +1,184 @@
# Apple Metal parity for issue #78
## GLM 5.3 Flash parity target
- Previous DS4 oracle: `8db89fe083ae4d17c9a2428ccd29803d3ae8f577`
- Current DS4 oracle: `ec7642cdd9ec81d01ad4b1fd8f8a3d1511533748`
- Main artifact: `GLM-5.3-Flash-Q2.gguf`, SHA-256
`e81fd6241c6e55a64e1e14e47a3eab61a173fa8d7e4b5c1d1848827119705b32`
- Vision artifact: `GLM-5.3-Flash-Vision-Encoder.gguf`, SHA-256
`ae23e14c6979e889051b2e4a39351abcdafb161e18e606fae4d8c40095a4bf3a`
- Machine: MacBook Pro `Mac17,7`, Apple M5 Max, 128 GiB unified memory,
resident Q2 weights
The applicable commits after the previous oracle are:
| DS4 commit(s) | Scope carried into DS4Server |
| --- | --- |
| `147109a` | GLM 5.3 shape, GGUF binding, KDA/DSA attention, hyperconnections, routed MoE, BF16 tensors, output head, integrated MTP, and Metal kernels |
| `4771329`, `9f95d9f` | Vision encoder, image preprocessing, prompt overlays, and compact-prefill correctness |
| `b0c31af` | Pooled indexer cache, bounded attention memory, and batched prefill |
| `747eafc`, `1df8ff0` | Fixed-image encoder oracle, replay/logit checks, and multimodal quality gates |
| `8adb221` | Directional attention and FFN steering at the DS4 residual points |
| `639d4eb` | Continued-prefill cache and position behavior |
| `6cf658a` | BF16 decode projection schedule and Metal kernel specialization |
| `01f3316`, `81c8c39`, `5da3206` | Current Metal compact-cache ABI, Q2/full-checkpoint binding, and GLM streaming compatibility |
CUDA- and ROCm-only commits in the range are excluded. The Rust release gates
compare three-run medians against this exact DS4 build, require identical
greedy token hashes, and fail only when Rust is more than 2% slower than DS4;
there is no upper bound when Rust is faster. Text measures an
8192-token frontier plus steady decode, both with and without integrated MTP.
Vision compares the fixed encoder embedding (cosine at least 0.995, mean
absolute error at most 0.001, maximum error at most 0.06), encoder latency,
multimodal prefill, steady decode, image-conditioned logits, and replay.
### GLM 5.3 Flash acceptance results
The text fixture is `ds4.c`, rendered as one no-system chat turn (`-sys ''` in
DS4), truncated to exactly 8192 tokens, with a 32768-token allocation and 64
greedy output tokens. Same-process three-run medians are used because both DS4
and DS4Server exhibit a load-order/thermal artifact with this 96 GB model.
| Capability | Metric | DS4 | DS4Server | Result |
| --- | --- | ---: | ---: | --- |
| Text | Prefill tok/s | 288.91 | 295.21 | Rust 2.18% faster |
| Text | Decode tok/s | 22.58 | 22.78 | Rust 0.89% faster |
| Integrated MTP | Decode tok/s | 26.55 | 26.80 | Rust 0.93% faster |
| Vision encoder | Fixed-image seconds | 2.6255 | 2.4354 | Rust 7.24% faster |
| Vision prompt | Prefill tok/s | 185.81 | 201.51 | Rust 8.45% faster |
| Vision prompt | Decode tok/s | 26.40 | 27.21 | Rust 3.09% faster |
Text and MTP produced the same token SHA-256,
`0b61e613708d8367fc87f5a32fbfd4ba098b40ee920645b1e38381e867131108`.
The 1094-token fixed-image prompt produced the same vision token SHA-256,
`424fdadc10c0a2bd95b405a3985151e84aa844a209d2a2ca72cd01c39dfa53db`.
The Rust vision embedding compared with the DS4 oracle at cosine
`0.999958726`, mean absolute error `0.000036702`, and maximum error
`0.006153620`.
The checkpoint advertises a 1048576-token maximum. On this 128 GiB machine,
32768 is the default manageable context and 50000 is the extended option: the
50000-token run used 3.51 GiB of live runtime tensors in addition to 90.61 GiB
of mapped model wrappers and retained the exact output hash. Larger allocations
leave too little operating-system and application headroom for a reliable
default.
## Frozen comparison
- DS4 oracle: `8db89fe083ae4d17c9a2428ccd29803d3ae8f577`
- DS4Server pre-issue baseline: `79468c65b68c28e1539acf0a2a14bd42e000e365`
- DeepSeek V4 Flash 0731 IQ2XXS/w2Q2K model SHA-256:
`ca22ae2f838e14077c22bc1c1417b71b45b5e5a3687bd96c2ac6e17fdb6261c0`
- DSpark support-model SHA-256:
`7e319924541db3f7a163ed7e11d7532a70d48228ab59d36cb81e1d4511885360`
- Machine: MacBook Pro `Mac17,7`, Apple M5 Max, 128 GiB unified memory,
macOS 26.6.2, power 100%
- Context and prefill allocation: 4096 tokens; quality, steering, speculative
decoding, and DSpark disabled for the throughput gate
- SSD mode: 4096 cached experts, 4096 popularity-preloaded experts, ordinary
(not cold) streaming
- Prompt: `Count from one to two hundred, spelling out every number.` rendered
to 16 tokens; 128 greedy non-EOS tokens are measured
The longer prompt is deliberate: all 128 generated tokens are reachable before
EOS and are identical between DS4 and DS4Server in each mode. A short-response
fixture reached EOS early and made a forced post-EOS speed run compare
unreachable, mode-dependent states.
## DS4 performance-change matrix
The audited range is
`efdadd41e20134af4f3381e1ed90e96fe4faef6f..8db89fe083ae4d17c9a2428ccd29803d3ae8f577`.
Documentation-only, parser/server-only, CUDA, ROCm, tensor-parallel, model
download, and merge commits do not affect the Rust-owned single-session Metal
graph. The applicable performance/runtime commits are accounted for below.
| DS4 commit(s) | DS4 call site or behavior | DS4Server result |
| --- | --- | --- |
| `3ce6777`, `8e0f997` | Native streamed-expert cache pinning and safe fallback | Active in the byte-matched native Metal cache; Rust preserves the cache budget and fallback. |
| `df04887` | `ds4_gpu_attention_indexed_mixed_batch_heads_tensor` | Active through `encode_layer_with_cache_rows`; the long-context fixture crosses the indexed boundary. |
| `74e8f11`, `4591cb1`, `a51e6ec`, `f284165`, `af80694`, `7fb2830`, `0e89a0e`, `769a8ba`, `8d683d6` | DSpark block commit, deterministic scheduling, acceptance replay/direct commit, greedy identity, and exact/opportunistic sampling | Active in `Dspark::propose` and the verifier commit paths; greedy and exact stochastic fixtures cover the frozen support model. |
| `96c3ba4`, `d69a017`, `81f8534`, `ac5cb2c`, `48392e3`, `222b2cb`, `532ec8b` | Metal 4 Q4 projection, compact routed-MoE, aligned Q8, indexed-attention, and routed prefill kernels | Active through the existing `encode_batch_layer` Q4/indexed/routed entry points; no second prefill graph was added. |
| `24903f3` | `ds4_gpu_head_rms_norm_rope_tail_tensor` | Existing Rust Q-head norm/RoPE fusion is retained and used. |
| `4893e0c`, `d14ce35` | Mapped-prefill expert seeding and consistent expert/non-expert maps | Rust seeds with `ds4_gpu_stream_expert_cache_seed_experts_gpu_copy`, falls back to CPU/pread, and restores one cached static decode map with the same non-routed spans. |
| `0dd0d36` | Release speed regression gate | Rust has an ignored release gate with retained raw CSV, DS4 reference threshold, baseline threshold, p50/p95, cache metrics, and token hashes. |
| `4e401a1`, `0e125e6` | Automatic pre-M5/M5 admission for exact decode fusions | Rust binds both device predicates and mirrors per-generation rollback variables without a UI preference. |
| `71d8c2a` | Q/KV pair plus ratio-4/128 compressor projection/store | `try_qkv_pair_compressor_fusion` calls the native combined entry point only for DS4's resident full-decode shapes. |
| `b64d3e0` | Emit-path compressor finalizer | `update_compression_pair` defers both updates only when the resident finalizer is eligible, then requires exact success. |
| `3826f52`, `ad4d05c` | Exact compressor softmax/product and ratio-4 pool | Native kernels are active; Rust's compressor ABI now passes the upstream `decode_one_token` and `defer_finalize` flags exactly. |
| `92d83e6` | HC norm/mix producer clustering | `decode_hc_mix` uses split-norm producer fusion, with the established norm/mix/split sequence as fallback. |
| `1be41c8` | Packed exact split-K attention reduction | Active in the vendored native attention kernel reached by Rust's decode attention calls. |
| `0e8fb54`, `e6a6ab5` | Router/select, shared gate/up, QKV norm/KV RoPE/store, parallel full FFN, inverse-attention RoPE, and pre-M5 ports | All applicable entry points are called with DS4 shape, tensor, quality, steering, profile, SSD, and device guards. Parallel FFN aborts on partial failure; inverse RoPE falls back when the attention kernel does not consume it. |
| `0658dac` | Long-context prefill/decode correctness | The 4096-crossing ignored fixture covers compressed/indexed state and the next decode token. |
| `023614e`, `0ad494e`, `42033ee` | Remove stream-512 experiment; make exact paths automatic; verify DFlash pipeline | No stream-512 or opt-in experiment remains. Feature rollbacks retain identical token hashes. |
| `24fa85e` | Keep successfully loaded disk KV checkpoints | Existing Rust checkpoint load/round-trip behavior is verified; only invalid checkpoints follow deletion policy. |
| `7694112` | Reuse a shorter exact GLM live prefix | `live_prefix_rewind_target` rewinds to `prompt_len - 1`, clears pending speculation, and reevaluates the last prompt token. |
| `5186e2c` | Keep Metal decode changes portable | Device checks and unavailable returns preserve non-M5 and unfused execution. |
The Metal MXFP4 commits (`725b084`, `1f862bd`, `7bec128`, `bcd1e57`,
`f75a065`, and their merge/format work) are intentionally excluded: MXFP4 is
issue #79. `ds4_gpu_set_decode_pipeline_fast_lookup` is reset to disabled,
matching DS4 because that cache is enabled only for the pre-M5 MXFP4 path.
## Results
Three implementations were run sequentially, three times per mode, with a
five-second cooldown. Medians are from
[`metal-parity-issue-78.csv`](metal-parity-issue-78.csv).
| Implementation | Mode | Prefill tok/s | TTFT ms | First decode ms | Stable tok/s | p50 ms | p95 ms |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| DS4Server baseline | resident | 69.63 | 256.07 | 26.37 | 39.53 | 24.88 | 26.61 |
| DS4 | resident | 56.49 | 305.94 | 22.71 | 45.60 | 21.91 | 22.32 |
| DS4Server issue #78 | resident | 69.76 | 252.01 | 22.66 | 45.60 | 21.94 | 22.33 |
| DS4Server baseline | SSD | 6.81 | 2476.94 | 125.30 | 8.63 | 114.93 | 124.28 |
| DS4 | SSD | 11.88 | 1393.54 | 46.75 | 26.11 | 35.62 | 46.86 |
| DS4Server issue #78 | SSD | 11.49 | 1440.79 | 48.84 | 25.07 | 37.70 | 48.85 |
TTFT is the per-run prompt prefill duration plus first decode latency; its
median is reported separately from the first decode kernel latency.
Resident stable decode improves 15.4% over the baseline and matches DS4.
SSD stable decode improves 190.3% over the baseline and is 4.0% below
DS4, inside the required 5% boundary. SSD cache accounting is identical to
DS4 for the measured token sequence: 4096 entries, 32,958 hits, 4,194 misses,
and 29,684,662,272 pread bytes. The resident and SSD token SHA-256 values are,
respectively, `ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d`
and `2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052`;
DS4 emitted the same 128-token sequences in the corresponding modes.
Representative `/usr/bin/time -l` measurements report:
| Implementation | Mode | Maximum RSS bytes | Peak footprint bytes | Process swaps |
| --- | --- | ---: | ---: | ---: |
| DS4 | resident | 78,790,656 | 4,746,511,440 | 0 |
| DS4Server issue #78 | resident | 129,024,000 | 104,071,816 | 0 |
| DS4 | SSD | 29,066,280,960 | 29,595,620,664 | 0 |
| DS4Server issue #78 | SSD | 29,098,393,600 | 104,350,344 | 0 |
The post-run system memory-pressure reading was 92% free. Every measured
process reported zero swaps; the post-run host counters were 0 swap-ins and 20
historical swap-outs.
## Reproduction
DS4 uses the exact 16 Rust-rendered prompt token IDs with its speed-benchmark
format. DS4Server uses the installed-model fixture:
```sh
DS4_REFERENCE_TPS=45.60 \
DS4SERVER_BASELINE_TPS=39.526849 \
cargo test --release --all-features flash_0731_m5_decode_performance_gate \
-- --ignored --nocapture --test-threads=1
DS4SERVER_BENCH_SSD=1 \
DS4_REFERENCE_TPS=26.11 \
DS4SERVER_BASELINE_TPS=8.634659 \
cargo test --release --all-features flash_0731_m5_decode_performance_gate \
-- --ignored --nocapture --test-threads=1
```
The gate fails unless throughput improves over the frozen DS4Server baseline
and remains at least 95% of DS4. `DS4SERVER_BENCH_TOKENS=1` prints the prompt
and generated sequences for direct DS4 comparison.
+104
View File
@@ -0,0 +1,104 @@
# Qwen3.8 Flash Next Bare Speed artifacts
DS4Server uses the text artifacts published at
`Youssofal/Qwen3.8-Flash-Next-MTPLX-Bare-Speed` revision
`74559cdf34fbfc0b593de72d17e93f37fd4f9ea7`. The upstream base is
`Qwen/Qwen3.8-Flash-Next` revision
`de4b8e4d43b917e7706784d8bb445c9af86a3540`, under the Qwen Community
License 1.0 included in the artifact set.
The source already separates the 71,742,682,599-byte resident text core,
32,000,154,008-byte PLE table, and 1,672,575,532-byte MTP head. DS4Server's
artifact conversion is therefore deliberately byte-preserving: the Rust tool
validates and materializes the selected files without changing their tensor
containers or numerical interpretation. The complete selected download is
105,438,842,430 bytes.
The manifest at `assets/models/qwen38-flash-next-bare-speed.json` pins every
selected file by byte size and SHA-256. Its companion TSV records all 2,527
text, PLE, and MTP tensors with their source file, name, dtype, shape,
quantization parameters, and byte range. Validation also compares the 2,466
core tensors with the source index, rejects duplicate names, invalid byte
ranges, and precision-map drift, and checks the fixed `qwen4_exp` architecture
and MTPLX serving contract before any payload is accepted.
The precision contract follows the pinned files and their per-tensor
`quantization_config`: affine 4-bit matrices use 64-weight groups; router
matrices and QSA `index_qk_proj` use the source's declared 8-bit groups; GDN
convolution and recurrent parameters, norms, hyper-connection parameters, and
the MTP mixing/norm parameters remain BF16. The MTP layer's attention and MoE
matrices remain in their published 2-, 4-, or 8-bit affine representation;
they are not up-converted to satisfy a broader prose description. The PLE
sidecar's own header pins its 4-bit rows to 32-weight groups. No Python, MLX,
llama.cpp runtime, or foreign converter is part of the tool or product.
`model-vision.safetensors` and its image/video processor configs are explicitly
excluded. The full config is retained because its `text_config` is the loader
authority. llama.cpp commit `b356fa2624643b6d5753162ae43efff8cdd4d8cb`
is not used as a converter because its reviewed path did not preserve this
separate MTP artifact.
Download and authenticate every selected file without loading tensors:
```sh
cargo run --release --bin qwen38-artifacts -- \
fetch assets/models/qwen38-flash-next-bare-speed.json OUTPUT_DIRECTORY
```
Interrupted downloads remain as resumable `.part` files. The fetcher uses the
manifest's immutable revision URLs, validates resumed ranges, authenticates
each completed file, and then runs the complete inventory and precision
verifier.
Verify a pinned checkout:
```sh
cargo run --release --bin qwen38-artifacts -- \
verify assets/models/qwen38-flash-next-bare-speed.json SOURCE_DIRECTORY
```
Materialize a clean, authenticated artifact directory:
```sh
cargo run --release --bin qwen38-artifacts -- \
materialize assets/models/qwen38-flash-next-bare-speed.json \
SOURCE_DIRECTORY OUTPUT_DIRECTORY
```
The output directory must not already exist. Materialization uses hard links
when possible and otherwise copies bytes, then re-runs the same full verifier
against the output.
## Application intake
The native Model Manager exposes the complete 29-file set as one resumable
artifact. It downloads from the immutable source revision above, verifies each
file before promotion, retains interrupted `.part` files, and removes the core,
PLE, MTP, tokenizer, metadata, checksum markers, and partials together.
Model selection is persisted as `qwen3.8-flash-next`; intake recognizes the
artifact from the pinned `qwen4_exp` metadata rather than its directory name.
Before Metal allocation, the Rust loader checks every configuration and runtime
contract in the manifest and all 2,527 safetensors records against the frozen
inventory. The tokenizer uses the pinned byte-level BPE, ChatML special tokens,
official low/medium/xhigh/direct formatting, grouped tool responses, and the
official temperature 1.0, top-p 0.95, and top-k 20 defaults.
At the native 262,144-token ceiling, admission reports the 71,742,682,599-byte
resident core, the separately mapped 32,000,154,008-byte PLE table, the
1,672,575,532-byte optional MTP artifact, 6,557,908,992 bytes of KV/recurrent
state, and 27,262,976 bytes of prefill transient storage. PLE mapping is not
counted as resident allocation, and MTP is counted only when enabled. Execution
still fails with an explicit backend-unavailable error until the Rust Metal
backend is delivered by issue #95; it never falls through to a GGUF executor.
## Verification receipt
On 2026-09-03 the pinned source passed full authentication and inventory,
precision, and representative-row verification. Two separately empty output
directories were then materialized and independently verified. Each contained
the same 29 selected files and 105,438,842,430 artifact bytes; normalized
SHA-256 listings for all 31 output files (artifacts, manifest, and inventory)
were byte-identical and had list digest
`3e0233c2356b26859de43b25b2a3df08169dd8263071432bdf2fc497da74007c`.
The six dequantized-row receipts are pinned directly in the manifest.
+146
View File
@@ -0,0 +1,146 @@
# Qwen3.8 Flash Next performance and parity receipt
Issue #99 optimizes the pinned `Youssofal/Qwen3.8-Flash-Next-MTPLX-Bare-Speed`
text model without changing its artifact precision, sampler, or token behavior.
All application and inference work remains Rust; native additions are confined to
the existing Objective-C Metal bridge and Qwen `.metal` kernels. Integrated MTP
is the Qwen production default; an explicitly stored user opt-out remains
authoritative.
## Result
The acceptance B1 median is **60.825779 tok/s MTP**. A later B2 confirmation,
after adding complete repair telemetry and under the then-current machine load,
measured **58.019436 tok/s**. Both sequences produced the same 64-token SHA-256
`46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72`, with 44
accepted drafts from 60 proposals in 20 verifier passes. The fresh matched MTPLX
median was 59.751170 tok/s, putting B1 1.8% above and B2 2.9% below the oracle;
both are inside the issue's 5% parity band. Raw per-run data is in
`docs/qwen38-issue-99.csv`.
Plain AR reached a 50.671077 tok/s median with stable token hash
`15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d`.
The published 47 tok/s MTPLX figure is contextual rather than a fresh matched
AR baseline: after the valid MTP oracle run, the installed MTPLX package and
checked-out source drifted apart. The current CLI rejects this Flash Next pack
as unsupported and the source path fails at the missing
`DecoderLayer.input_layernorm`, so no invalid number is presented as a fresh AR
comparison.
The published 75.9 tok/s MTP result is from a different MTPLX runtime/workload.
With identical pinned artifacts, 4,096 context, prompt, sampling, seed, and
64-token measurement window, the current usable oracle result is 59.751170
tok/s. The remaining gap is therefore not explained by quantization drift or a
different output distribution.
Maximum fan control could not be established because neither ThermalForge nor
TG Pro is installed; MTPLX reports its fan boost as unavailable. Both compared
engines ran under the same no-boost constraint. No system utility was installed
or substituted for the missing control.
## Fixed workload
- Hardware: MacBook Pro `Mac17,7`, Apple M5 Max, 18 CPU cores, 128 GiB unified
memory.
- OS: macOS 26.6.2, build 25G83, arm64.
- Model revision: `74559cdf34fbfc0b593de72d17e93f37fd4f9ea7`.
- Model manifest contract SHA-256:
`6f1172de47fa30b9602e13fc7ad14e578a813bb5b1fab8f0ef320041aac6c19a`.
- Tensor inventory SHA-256:
`b5731e6febcf865d276a0e7b144da02375f2d7e3129594f271ee9c6c351f4c8f`.
- `config.json` SHA-256:
`826302af9ff3293cfc9975c9a5a96546fa0aa9f0808505adfc30a62fef806965`.
- `mtplx_runtime.json` SHA-256:
`381c7666872f878542494a48a874bb0784c72a955d18cc077324d3c9e74b8cdb`.
- Prompt: `Write a concise Rust implementation of binary search and explain its invariants.`
- Prompt tokens: 26; context: 4,096; generated tokens: 64.
- Sampling: temperature 1.0, top-p 0.95, top-k 20, min-p 0, seed 12,345.
- One process, three runs: first run cold, following runs warm.
The exact DS4Server commands were:
```sh
DS4SERVER_QWEN38_SOURCE=/private/tmp/qwen38-bare-source \
DS4SERVER_QWEN38_MEASURED=64 DS4SERVER_QWEN38_RUNS=3 \
cargo test --release qwen_m5_performance_gate -- --ignored --nocapture
DS4SERVER_QWEN38_SOURCE=/private/tmp/qwen38-bare-source \
DS4SERVER_QWEN38_MTP=1 DS4SERVER_QWEN38_MEASURED=64 \
DS4SERVER_QWEN38_RUNS=3 \
cargo test --release qwen_m5_performance_gate -- --ignored --nocapture
DS4SERVER_QWEN38_SOURCE=/private/tmp/qwen38-bare-source \
cargo test --release qwen_mtp_matches_target_and_restores_its_cache \
-- --ignored --nocapture
```
The MTPLX comparison used its normal `serve` path with the same local Bare Speed
directory, generation mode, prompt, context, sampler, seed, and single request
stream. A later retry is not a valid benchmark because the local MTPLX
source/package compatibility gate fails as described above.
## MTPLX logic review
The production path now follows the MTPLX/MLX graph rather than retaining the
temporary Qwen experiments used while earlier issues established correctness:
- PLE uses the artifact's 4-bit, group-32 rows, streams them with `pread`, keeps
a bounded hot cache, and publishes hit, miss, eviction, byte, and read-time
counters. The warm MTP runs had 2,080 hits, no misses, and no reads.
- GDN uses fused input projection, MTPLX-equivalent conv+SiLU+L2 normalization,
BF16 rounding points, decay/delta recurrence, gated norm, and direct verifier
capture buffers. Rejected rows restore or advance authoritative recurrence
rather than inventing state.
- QSA uses the MTPLX `qsa_flash_skip` row ownership, online-softmax merge, and
native 256-thread/eight-SIMD layout. Long-context state remains linear and
admitted through 262,144 tokens without a gather-copy fallback.
- MoE uses the exact router ownership and merged gate/up projection. The custom
verifier MoE experiment and NAX path were removed because they are not the
oracle's promoted path.
- MTP uses the same checkpoint, history, proposal, verifier, rejection-repair,
full-accept bonus, and NumPy-compatible PCG64 sampling rules. The exact cache
restoration test passes after the final telemetry change.
- Sparse top-k sampling stays on device for the supported official sampler and
uses the existing exact CPU path only when the requested sampler is outside
that kernel's contract. Full production logits are not copied to the CPU.
- Capability fallbacks remain exact implementations, not alternate Qwen
behavior. Experiments that changed tokens (normal Metal math, wider Tensor
QMM, and recurrence-only repair) were removed rather than left dormant.
The 4-bit/group-64 core, declared 8-bit router/QSA matrices, BF16 recurrent and
normalization tensors, 4-bit/group-32 PLE, and published 2/4/8-bit MTP tensors
are consumed as stored. No blanket up-conversion or replacement quantization is
used. Peak RSS in the final MTP sequence was 52.61 GB, down from the original
102.65 GB path; the 105.44 GB artifact size includes a 32.00 GB file-backed PLE
table that is not resident wholesale.
## A/B evidence
The optimization work used exact hashes or tensor comparisons before promotion:
| Area | Before | After | Decision |
| --- | ---: | ---: | --- |
| Resident memory | 102.65 GB RSS | 52.61 GB RSS | Reused staging and streamed PLE promoted |
| Router | 49.7 ms GPU | 18.7 ms GPU, 23.4 ms wall | Row-owned reduced router promoted |
| 16K QSA AR | 2.6 tok/s | 45.7 tok/s | MTPLX flash-skip rewrite promoted |
| Shared verifier gate/up | 54.7 tok/s | 58.3 tok/s | Exact fused projection promoted |
| Direct GDN verifier capture | 58.5/58.7 tok/s | 60.8 tok/s | Direct capture buffers promoted |
The direct-capture B1 acceptance was followed by the B2 confirmation in the CSV;
both retained the same token hash. Normal Metal math, Tensor QMM at verifier
width, and shortened recurrence repair failed exact-oracle checks and were
removed. Custom MoE verify, fused GDN output projection, and async AR were not
promoted because the MTPLX evidence did not show a win and DS4Server profiles did
not justify overriding that result.
## Context and verification
The long-context gate exercised 2,048, 16,384, 65,536, 131,072, and the native
262,144-token maximum. AR remained 43.488 tok/s at the maximum, and live state
grew from 58,195,968 bytes at 2K to 7,449,083,904 bytes at 262K without collapse
or over-admission. The CSV records every context result and state size.
Final issue verification comprises the exact MTP cache/token oracle, affine
QMV/QMM MLX reference comparisons, focused Qwen tests, the long-context state
gate, the release performance gate, formatting, Clippy with warnings denied, the
macOS application bundle, and the full all-features Rust test suite.
+295
View File
@@ -0,0 +1,295 @@
# DS4Server User Guide
DS4Server is a native macOS application for running supported DwarfStar models
locally. Model files, chats, project references, settings, checkpoints, and
agent-extension data stay on this Mac.
## Getting started
1. Open **View > Model Manager** (`⇧⌘M`). Download a supported model and any
acceleration artifact you intend to use, or verify an artifact already on
disk. A model cannot run until its complete file passes verification.
2. Choose **Add project** and select the folder the agent should work in. The
project remains in its original location; DS4Server stores only its reference
and chat data.
3. Choose **File > New Chat** (`⌘N`). Select a thinking profile and a shell
permission mode in the composer.
4. Enter a request and send it. The choices match the selected model: DeepSeek
offers **Direct**, **Think Low**, **Think High**, and **Think Max**; GLM 5.2
offers **Direct**, **Think High**, and **Think Max**; GLM 5.3 Flash always
thinks and offers **Think Low**, **Think High**, and **Think Max**.
The selected model is shared by the app and the optional local HTTP endpoint.
Preferences are saved locally and generation settings are scoped by model and
thinking profile where the controls indicate that.
## Projects, chats, and context
The sidebar switches projects and chats. Project controls add, rename, or delete
a project reference. Deleting one removes its DS4Server sessions and
checkpoints, but never deletes the referenced folder. An unsaved draft can move
to another project; a saved chat remains attached to its original project.
Chat controls can rename, pin, archive, restore, compact, rebuild, export, or
delete a conversation. Archived chats remain under the project's archive until
restored or deleted. Deleting a chat permanently removes its transcript and KV
checkpoint. **File > Export Chat as Markdown** (`⇧⌘S`) or **Export Chat as
HTML** saves the visible conversation, including clearly labeled user,
assistant, thinking, embedded content, and tool sections while excluding
internal system messages. Markdown keeps embedded content as source code;
HTML renders validated SVG figures and A2UI surfaces inline in a self-contained
dark document. Tool-call JSON is collapsed by default and can be expanded from
its disclosure row.
Send another prompt while a response is active to queue it for that chat.
**Stop** cancels current model and agent work. You can work in several chats at
once: each keeps its own transcript, prompt queue, tools, approvals, context,
and interactive surfaces. Independent tool work may overlap, but model
inference is serialized through one shared runtime so weights are not duplicated.
Quitting with active chats asks whether to stop them first.
The context indicator shows used and available tokens. Near the limit,
DS4Server compacts older model context while retaining the full visible
transcript. Saved chats also keep local KV checkpoints for fast continuation.
A checkpoint belongs to the exact model, quantization, context size, and runtime
configuration that created it. After one of those changes, DS4Server reports a
context rebuild, prefills the saved transcript, and writes a compatible
replacement without losing messages. **Rebuild context on next use** requests
the same safe rebuild manually.
## Agent capabilities and safety
The coding agent can use native tools to list a directory, read files, search
file contents, write or edit files, inspect Git state, and maintain Dev Brain.
It can also run shell commands and use web search or visit pages when the task
needs them. The native glob tool finds files recursively by basename or relative
path pattern without a shell; content matching remains the search tool's job,
and a directory listing remains a non-recursive view of one folder.
Tool calls are checked against typed schemas before execution. Unknown tools,
missing or extra fields, malformed calls, and invalid values return structured
errors instead of running. Results appear in the transcript. Copy actions expose
their complete text, and large output may also be saved to a local file rather
than filling the transcript. **Stop** cancels active generation and cancellable
tool work.
The shell permission mode is stored with each chat:
- **Heuristic** applies DS4Server's built-in command-risk checks.
- **AI based** asks the local model to classify a shell command in a separate
one-shot request. A risky result shows its reason. If classification fails or
is invalid, approval is required.
An approval dialog shows the command and working directory. **Allow once** runs
that operation; **Deny** returns the refusal to the agent. Destructive, network,
privileged, application-control, credential, and outside-trusted-path commands
require approval as applicable. Dev Brain's configured vault is trusted like
the project for ordinary path checks, but does not bypass those risk classes.
### Ralph loops
Ask the agent to use a Ralph loop when a bounded task benefits from repeated
autonomous passes, for example, “Use a Ralph loop with at most 12 rounds to fix
and verify this parser.” Ralph is an agent tool, not a view or slash command.
Its default is 8 rounds and its allowed range is 164.
Each round receives the same objective in a fresh model context, inspects the
current project, and leaves file changes on disk. The next round receives only
the previous validated report, applicable workspace instructions, and available
skills. The current model settings and permission mode still apply. Ralph stops
on completion, a reported blocker, a failed round, cancellation, or the round
limit; a worker's completion report is evidence to review, not independent
certification. Ralph workers cannot start nested Ralph loops.
### Workspace instructions and standard skills
DS4Server reads global instructions from
`~/Library/Application Support/de.rfc1437.ds4server/AGENTS.md`, then project
instructions from root to the working directory. In each directory,
`AGENTS.local.md` overlays `AGENTS.md`. Deeper guidance wins over broader
guidance, while system, developer, and direct user instructions remain higher
priority. Additions, changes, and removals are tracked during a chat and restored
after compaction; an unreadable changed file leaves its last readable version in
effect with a notice.
Manually installed standard skills live at
`~/.agents/skills/<name>/SKILL.md`. DS4Server initially gives the agent each
valid skill's name, description, and location; the agent reads the full skill
only when the task matches. These roots are readable but not writable through
agent file tools. Start a fresh chat after changing the manually installed skill
set so its prompt and tool roots agree.
### Dev Brain
Dev Brain is a project-backed Obsidian wiki for durable, source-cited engineering
knowledge. In **Preferences > Dev Brain**, select an existing vault containing
`.obsidian`, enable it, and save. Ask the agent to build, refresh, search, or
validate the wiki. DS4Server manages its contract files and explicitly marked
topic pages, not Obsidian settings, hidden files, attachments, or unrelated
notes. Verified pages can support answers; stale or needs-review pages are leads
that the agent must recheck against project source. Disabling Dev Brain removes
its tools and guidance without deleting the vault.
## Agent extensions
**Preferences > Agent Extensions** installs and manages portable Codex plugins:
1. Enter an HTTPS Git repository URL and, optionally, a branch, tag, or commit.
2. Choose **Install**. The repository must contain
`.codex-plugin/plugin.json` and provide at least a skills directory or a
supported lifecycle-hooks manifest.
3. Inspect the displayed description, source URL, requested ref, resolved
commit, author, version, skill count, and hook names.
4. Enable the extension. An extension with command hooks first asks for explicit
trust. Its programs run with your user permissions even though DS4Server
invokes them without a shell and limits their environment, runtime, input,
and output.
5. Use **Update** to fetch the configured source/ref, disable the toggle to stop
future use, or **Uninstall** to remove the installed package and its stored
extension data. Hook failures appear on the extension row.
Enable, disable, and update changes apply to subsequent matching hook events.
A current chat does not retroactively receive a `SessionStart` event. Because a
chat's native tool roots are initialized for that session, start a fresh chat
after adding, updating, enabling, or disabling extension skills. An app restart
is not otherwise required.
Extension skills are versioned inside their plugin and are managed with its
toggle, update, and uninstall actions. They are separate from manually installed
standard skills under `~/.agents/skills`. Installed packages, per-extension
data, and `registry.json` live under
`~/Library/Application Support/de.rfc1437.ds4server/extensions/`.
## A2UI interactive surfaces
A2UI is an interactive or graphical response the agent can generate when it is
more useful than prose. It is not another model and is not a tool you operate
directly. Enable or disable it in Preferences. Ask for an interactive chart,
table, form, map, or other surface, then open the **A2UI** view (`⌘2`) if the app
does not switch there automatically.
Buttons can send events back to the agent. Text fields, checkboxes, sliders,
date/time inputs, and choice pickers update the surface's data. Tabs and modals
organize content. Previous and next actions navigate saved surface history;
**Dismiss** removes the current saved surface after confirmation. Ordinary model
prose remains in the Chat transcript. Video and audio controls open native
playback windows.
The embedded catalog currently supports:
- Content: Markdown/text, images and avatars, icons, video, audio, and dividers.
- Layout: rows, columns, lists, cards, modals, and tabs.
- Controls: buttons, text fields, checkboxes, sliders, date/time inputs, and
single- or multiple-choice pickers.
- Visual and research components: bar, line, area, stacked-bar, pie, donut, and
heatmap charts; tables; metrics; timelines; maps; mind maps; and forms.
## The rest of the app
### Models and preferences
**Model Manager** downloads, resumes, verifies, and removes managed model and
acceleration artifacts. Removing an artifact does not remove projects or chats.
Preferences select the active model and its default thinking profile. Generation
values are stored per model and thinking mode; acceleration and SSD settings are
stored per model; the custom system prompt is shared across profiles. Other
sections control A2UI, permission defaults, endpoint settings, Git diff display,
Dev Brain, extensions, checkpoint storage, and diagnostics.
A new draft uses the currently selected model when its first turn is persisted.
From then on the session keeps that exact model, so opening it immediately selects
the same model and that model's thinking choices. Legacy sessions recover the
exact model from their checkpoint when available; a legacy session whose
exact model can no longer be identified remains viewable but cannot be continued.
### Main views, sidebar, and branches
**Chat** shows the conversation, **A2UI** shows interactive surfaces, **Git**
shows the current worktree, and **Stats** reports model, generation, HTTP, SSD,
and KV-cache activity. **View > Show Sidebar** (`⌘B`) hides or restores the
project sidebar. The composer status shows the active project and its local Git
branch. Branch switching is available while that project's chats are idle.
### Git
The Git view separates staged and worktree changes. Select files to stage,
unstage, or commit; selecting a file opens a colored unified or side-by-side
diff. Committing with no selected or staged files asks before staging and
committing every change. Fetch, pull, and push use the `origin` remote and report
authentication, hook, conflict, and other Git errors.
### Local API
Preferences can enable an OpenAI- and Anthropic-compatible endpoint on
`127.0.0.1`, port `4000` by default. Endpoint conversations belong to their
client and do not appear in the sidebar. CORS is off by default; enable it only
for trusted browser clients on this Mac. Saving changed endpoint settings
restarts the local listener.
`reasoning_effort` accepts only the selected model's advertised values from
`GET /v1/models`: DeepSeek accepts `none`, `low`, `high`, or `max`; GLM 5.2
accepts `none`, `high`, or `max`; GLM 5.3 Flash accepts `low`, `high`, or `max`.
Omitting it uses the upstream model default: `low` for DeepSeek and `max` for
both GLM models. Unsupported values and conflicting thinking controls return a
400 error instead of being converted to another effort. This per-model thinking
selection is intentionally part of the DS4Server agent harness, not a DS4 parity
surface; do not replace it with mapped, hidden, or shared choices.
### Data and recovery
Application data is under
`~/Library/Application Support/de.rfc1437.ds4server/`: `config.yaml` stores
preferences, `data.sqlite3` stores projects and chats, `models/` stores managed
artifacts, `kv-cache/` stores checkpoints and metrics data, `extensions/` stores
plugins, and `browser/` stores the agent browser profile. Project source remains
where you selected it.
Removing a model can be recovered by downloading or restoring and verifying the
same artifact. A context incompatible with changed runtime settings is rebuilt
from the saved transcript. Deleted project references, chats, extension data,
and their checkpoints are not restored by DS4Server; use your own backup if you
need recovery from deletion.
## Troubleshooting
- **No model selected or model unavailable:** select a model in Preferences,
then download and verify all required artifacts in Model Manager.
- **Verification failed:** retry verification or download. DS4Server will not
load incomplete or hash-mismatched weights.
- **Context is rebuilding:** the saved checkpoint no longer matches current
model/runtime settings. Let the transcript prefill finish; messages remain.
- **Extension install failed:** use an HTTPS Git URL and verify the repository
contains a valid portable plugin manifest plus skills or supported hooks.
- **Extension hook failed:** read the error on its Preferences row, repair or
update the extension, then retry the matching event. Trust is required before
command hooks can run.
- **Tool approval denied:** the operation was not run. Send a safer request or
repeat it and approve only after checking the command and working directory.
- **A2UI is unavailable:** enable A2UI in Preferences and ask the agent for an
interactive surface in a new prompt.
## Keyboard shortcuts
These are the shortcuts registered by the native DS4Server menu. On Macs whose
top row controls brightness or media, use `fn-F1` when macOS requires it;
DS4Server does not override the system keyboard mapping.
| Shortcut | Action |
| --- | --- |
| `F1` | DS4Server User Guide |
| `⌘,` | Preferences |
| `⇧⌘M` | Model Manager |
| `⌘N` | New Chat |
| `⇧⌘S` | Export Chat as Markdown |
| `⌘B` | Show or hide the sidebar |
| `⌘1` | Chat view |
| `⌘2` | A2UI view |
| `⌘3` | Git view |
| `⌘4` | Stats view |
| `⌘Z` | Undo |
| `⇧⌘Z` | Redo |
| `⌘X` | Cut |
| `⌘C` | Copy |
| `⌘V` | Paste |
| `⌘A` | Select All |
| `⌘Q` | Quit DS4Server |
@@ -0,0 +1,519 @@
# DeepSeek standalone-reference follow-up — 2026-09-11
The original antirez/ds4 process is used only under the user's explicit
reference-benchmark authorization. No original C engine is linked into DS4Server.
Evidence: `local-eval-results/deepseek-paired-20260911.nyUCsL/`.
**Measurement qualification (user clarification, 2026-09-11):** The user was
watching videos in parallel, using part of the GPU. The exact overlap with each
earlier run is not recorded. The current sequential comparisons are therefore
not controlled idle-device performance acceptance or causal before/after proof.
Their outputs and functional checks remain useful; timing receipts are retained,
not discarded. Do not attribute every gap to video playback or claim thermal
throttling from this information. Further throughput acceptance runs are deferred
until competing GPU activity can be controlled; code work and functional tests
can continue. This does not waive the2% requirement.
## Initial pair is not performance-parity evidence
Both AR processes finished the full Summary README → lighthouse Story → Python
`is_prime` conversation after a separate32-token OK warmup. Installed0731 GGUF
only, power100, Low, temperature0.6/top-p0.95/min-p0/top-k0/seed42, context32768,
quality/SSD/canary/DSpark off. Startup30s/progress45s,112GiB process-memory guards,
no total timeout. No builds or other model tests overlapped the GPU runs.
Two mismatches invalidate this initial pair (`ar-{reference,rust}.*`):
- The standalone driver requested2048 prefill rows, whereas the UI/harness
requested automatic (0), which the DeepSeek executor resolves to4096 at this
context. Original DS4's automatic setting also resolves to4096. The reference
driver now requests0 and records the public session prefill-cap value instead
of silently assuming the same chunk for all models. GLM's automatic request
remains unchanged. No product chunk reduction or special harness path.
- The same first prompt contains2741 tokens in the original but2742 in the
production runtime. The direct Rust tokenizer and all three original
continuation fixtures agree exactly; the mismatch occurs later, in the shared
cached-prompt renderer. A valid system-bootstrap tag with empty chat history
took the continuation branch and prepended an EOS before the first user turn.
Both UI and headless generation use this function.
## Root-cause correction and regression
`render_text_prompt` now requires nonempty history for the cached continuation
branch. A first user turn uses canonical full rendering, while `align_prompt`
still reuses its prepared system prefix. Real ongoing histories retain the
existing append-only behavior. No artificial wait, power change, kernel change
or KV-persistence rewrite was introduced.
The CPU-only `ds4_chat_matches_original_session_tokens` regression loads only
GGUF metadata/tokenizer and original recorded token IDs. It first checks the
three-turn tokenizer contract, then invokes the same shared renderer with the
actual system-bootstrap frontier and tag. Before the fix it fails with:
```text
shared bootstrap renderer differs: 2742/2741 tokens;
starts [0, 1, 128803, 45585]/[0, 128803, 45585, 260]
```
`token-before.*` is the passing standalone-tokenizer diagnostic;
`bootstrap-before.*` is the deliberately failing shared-runtime regression.
Neither file is overwritten or treated as a successful full parity run.
The test also accepts the saved GLM fixture to cover the other shared consumer.
Reproduction after building release tests:
```sh
DS4SERVER_CHAT_REFERENCE=local-eval-results/deepseek-paired-20260911.nyUCsL/chat-token-fixture.json \
target/release/test-supervisor 2048 30 30 \
target/release/deps/ds4_server-6141856e5c0fa6df \
ds4_chat_matches_original_session_tokens
```
The fixed renderer passes both saved DeepSeek and GLM three-turn fixtures
(`{chat,glm-chat}-token-fixture-fixed.*`,0.04/0.05s). Release all-target/all-feature
build, warnings-denied Clippy, rustfmt/diff checks,17 enabled sampling tests and
two enabled tokenizer tests pass. The model-dependent tests remain explicitly
ignored by default and were invoked with the recorded local fixtures above.
CLI SHA256: `9fb13c7f9049c3b5059b4fe79b68bf997dbe4e1bfdc4c631c130595cab8c359a`.
Standalone driver SHA256:
`ad7a4b94a5918505c58130d336d026e2c5b9640aab717ef185808bebd7536a25`.
## Corrected complete AR pair
`fixed-ar-{reference,rust}.*` and `fixed-ar-comparison.json` contain the new
original-then-Rust pair. The original session confirms prefill-cap4096 and
engine power100. Every text/thinking/token/cache comparison passes; all turns
end naturally. Counts are826/1444/199, prompts2741/3587/5057 and cached1/3567/5031.
| Turn | Rust / original engine-prefill ms | Rust / original decode t/s |
| --- | ---: | ---: |
| Summary | 6561.316 / 5425.137 | 36.916 / 37.813 |
| Story | 286.826 / 290.380 | 32.712 / 34.246 |
| Python | 334.486 / 369.018 | 32.020 / 32.734 |
This is functional agreement for the complete AR workload, **not** performance
acceptance: all three decode ratios are below98% in this single pair, and the
Summary prefill is slower. Different natural outputs in the invalid initial
pair must not be used as before/after throughput evidence. No GUI/canary run was
performed in this clean series.
## DSpark pair: functional comparison fails
Both full processes finished naturally and both watchdogs exited successfully.
The final comparison exits1 deliberately (`dspark-comparison.json`); this is
not an inference crash. Rust confirms opportunistic sampling, confidence0.6,
strict/exact off; the original loads the installed three-stage/block5 support
model and enables direct verifier commits.
The first prompt/cache counts agree (2741/1), but its output already diverges:
647 Rust tokens versus1208 original tokens. Thus later speed ratios compare
different histories and **cannot establish matched-work performance parity**.
Counts in Story/Python are1675/195 versus1455/347. The raw rates are retained
in the comparison file, not promoted to valid performance results.
An independent frontier defect is visible even within Rust's own history:
after2741 prompt +647 output tokens, the next cache is3389 instead of3388;
after3409 prompt +1675 output tokens, it is5085 instead of5084. A speculatively
returned EOS remains committed. The shared consumer currently rewinds only
GLM, while original `ds4_session_rewind` also truncates the DeepSeek logical
frontier and invalidates DSpark capture. This is separate from the now-fixed
first-prompt bootstrap EOS and from the first-answer decode divergence.
### Next cohesive DSpark work unit
1. Locate the first divergent cycle with identical recorded target history:
compare proposal IDs/confidence, GPU row argmax, accepted prefixes and
compression/DSpark capture state against original DS4. Cover full acceptance,
partial acceptance and rejection before declaring the full implementation
equivalent. Existing target-owned self-tests alone are not an oracle proof.
2. Match the consumer's EOS frontier and capture invalidation using DS4's
DeepSeek contract, including an accepted-EOS transition into the next user
turn. Do not transplant GLM's KDA rollback or merely hide the extra count.
3. Remove verified extra verifier work as part of that same comparison:
Rust `eval_batch_inner(collect_tops)` reads/clones every row's full vocabulary
and computes argmax on the CPU. Original opportunistic verification passes
`row_logits=NULL`, obtains GPU row tops, then reads only the committed row
(`ds4.c:6911269124,6915369159,6921069216`). Preserve the separate exact-
sampling requirements and verify row/stop decisions before timing the change.
Also verify per-turn counter scope across warmup/resident switching before
using cycle/acceptance ratios as exclusive stage measurements.
The four reviewed AR/DSpark Python answers (both implementations) each pass
their five generated assertions and5011 independent cases in isolated Python
with restricted builtins (`python-quality.jsonl`). No model-generated tools
were invoked. This quality check does not waive DSpark's token/state mismatch.
The six-cell performance goal, repeated timing acceptance and actual UI
responsiveness acceptance remain open. The user's working bundle is unchanged.
All model/reference/canary processes from this follow-up have terminated.
## DSpark cycle/state audit (second work unit)
Evidence: `local-eval-results/ds4-cycle-audit-20260911.vfsUc8/`. The original
source and installed artifacts remain the same. `DS4_SPEC_CYCLE_TRACE=1` is an
opt-in diagnostic in the shared production consumer and standalone reference
driver; it records the sampled first token, returned IDs, generated count and
committed position. It is off by default. Original `DS4_DSPARK_SPEC_LOG=1`
adds draft lengths, full/partial acceptance and scheduler decisions without
changing the graph. These traced runs are **not clean throughput acceptance**.
The initial complete traced pair reproduces the failure. In the first Summary
cycle, both targets sample671; Rust proposes12275 and emits only671, while DS4
proposes3967 and emits `[671,3967]`. The original warmup first differs in the
fifth returned cycle, after earlier partial accepts. This is not evidence of
a context-size or power-setting cause.
### Consolidated findings and implementation tasks
This table scopes the inspected path to the installed0731 DSpark support,
resident M5 Max execution and default opportunistic sampling. It does not claim
completion for arbitrary SSD/strict/exact modes or the six-cell performance goal.
Original line references below are for pinned `ds4.c` at `ec7642c`.
| Area | Original behavior / Rust discrepancy | Current action |
| --- | --- | --- |
| Initial support KV | `3262732821`: target HC expansion, per-stage HC mixing and attention normalization precede KV projection. Rust projected stage-0 normalized hidden directly to KV. | Implemented the missing operations using existing Metal calls and batch scratch. First Summary proposal now matches. |
| Single accepted draft | `36205` and `69112`: even one draft uses the target batch verifier. Rust used scalar decode, with different arithmetic and no captured suffix for the next support cache. | Removed the resident single-draft scalar branch. First two Summary cycles then match. |
| Verifier capture | `2795128053`: capture contains the preceding seed row plus all verified target rows. Rust retained only the suffix. | Added seed-row capture with the original slot stride and sufficient workspace. |
| Cache timing and gaps | `6666566732`: seed from the captured batch only when its end equals the next proposal position; otherwise crop/reset the absolute cache window. Ring maintenance does not create or bridge a window (`33329`, `27712`). Rust eagerly seeded each batch, extended windows after failed proposals and tracked only modulo positions. | Added deferred seed ownership and absolute window start; no false append after rejected proposals. Replaced batch workspaces are released before allocation of the next batch. |
| Partial acceptance | `6922169240`: ordinary partial commits invalidate row and batch captures, preserve compressor-prefix state, and do not claim support KV rows. Rust retained the captures and extended support counters; its old seed scratch also overwrote captured hidden rows. | Invalidate the deferred capture, preserve the original cache frontier, and stop overwriting hidden capture storage. Three and then32 recorded Summary cycles pass. |
| Scheduler | `53500`, `65215`: reset per request. `53666`: a window pause replaces, rather than maximizes with, the cold no-draft pause. | Reset in shared prompt alignment; regression covers7→4 and clearing prior-request counters. |
| EOS consumer | `73690`: logical DeepSeek rewind plus capture invalidation, not GLM KDA rollback. | Shared consumer now invokes the model-specific DeepSeek rewind; invalid forward rewind is rejected before mutation. Full next-turn verification still required. |
| Q8 confidence arithmetic | `34350` calls `matvec_any`, which dispatches type8 to activation-quantized Q8 matvec (`81448170`). The installed confidence tensor is Q8_0 `[4352,1]`; Rust instead used the dequantized-weight × F32-input reference calculation. | Quantize activations for the confidence dot too, and match the two alternating four-lane ARM SDOT/FMA accumulators (`7510`) in both confidence and Markov scoring. CPU regression distinguishes the quantized result from the old F32-input dot. |
| Confidence/output head | `6678066835`: check first confidence before doing the vocabulary projection; read later rows only as required. Rust calculated all base logits and read all rows before the check. | Implemented the early gate and per-needed-row readback. Confidence-disabled execution keeps hidden/head submission fused. Full1755-cycle comparison remains exact; clean timing acceptance is separate. |
| Verifier output | `3632536431`, `69153`, `69210`: fuse head into the layer command sequence, obtain GPU row tops, read only the committed logits row. Rust drained before the head and cloned all vocabulary rows for CPU argmax. | Implemented fused submission, existing GPU argmax/top-k reductions and committed-row readback for the ordinary verifier. Exact sampling retains all target distributions. Full1755-cycle default-mode comparison remains exact. |
| CPU Markov workers | `3388933963`: persistent helper pool and fused Q8 argmax; `1912` defaults to min(online CPUs,12), including the caller. Rust created scoped OS threads on every dense argmax, using all18 logical CPUs here. | Implemented persistent Rust workers with the existing Q8 arithmetic, identical contiguous row partition and ordered first-tie reduction. The caller executes slot0; default12 total threads and positive integer `DS4_THREADS` overrides up to32. All1755 original cycles remain exact; timing acceptance remains separate. |
| Verifier allocations | Original graph retains verifier scratch and GPU frontier/prefix buffers; Rust constructed `BatchScratch` and snapshot buffers for every verifier. | Reuse baseline and high-water prefix buffers, plus one verifier batch per existing padded row shape. Scratch is recycled only after delayed seeding or capture invalidation. Extended32-cycle oracle regression proves native-buffer identity reuse and byte-exact rollback; full-chat and timing evidence below. |
| Acceptance telemetry | Rust's executor-wide cycle counts and resident support counters have different ownership scopes. | **Open:** reconcile request/session counters before deriving acceptance ratios or exclusive stage percentages. |
Excluded after checking the guards: seed-batch fusion defaults to the ROCm
gfx1151 path, not this Mac (`53412`); the Markov GPU branch is under
`#ifndef __APPLE__` (`34266`); adaptive extra decode splits are pre-M5-only.
The output-head padding to8 rows is present in DS4's helper (`26086`) and is
not an unmatched Rust optimization. None of these were blindly enabled/removed.
### Runnable regression evidence
`dspark_matches_original_summary_cycles` loads only the installed GGUFs and
the recorded original JSONL, prepares the actual bootstrap boundary, samples
with the shared DS4 sampler and verifies each cycle's IDs and position. It is
ignored by default and explicitly supervised when run:
```sh
DS4SERVER_DSPARK_REFERENCE=local-eval-results/ds4-cycle-audit-20260911.vfsUc8/before-reference.jsonl \
target/release/test-supervisor 114688 30 45 \
target/release/deps/ds4_server-6141856e5c0fa6df \
dspark_matches_original_summary_cycles
```
- `cycle-test-before.*`: fails at generated2, Rust `[10059]` versus original
`[10059,260,13672,294,270]`.
- `cycle-test-batch.*`: after single-row batch verification, fails at generated7,
Rust `[4496,3051,943,30941,22]` versus original `[4496,3051,943,30941]`.
- `cycle-test-capture.*`: all three unchanged expected cycles pass (7.28s).
- `cycle-test-32.*`:32 original cycles, prior-request scheduler reset and
safe logical rewind pass (9.32s). These are functional diagnostics, not
stories truncated for a throughput comparison.
The original full pair, intermediate cache-only full run and failing receipts
are retained. No original C implementation was added to the application; no
model download, bundle replacement, commit or push was performed.
### Confidence follow-up from the full chat
The subsequent `fixed-rust.*` full run matches451 complete returned cycles
(including warmup), then differs at Summary generated1018. The first divergent
decision is one cycle earlier: at position3759, original DS4 proposes `[588,699]`
and rejects the first draft, while Rust's confidence logit0.40281284 falls below
the0.6 sigmoid threshold and suppresses drafting entirely. The different
no-draft scheduler decision then changes the returned cycles. All turns still
end naturally, with counts1194/1424/296 and **no extra cached EOS**; this is
progress, not a matched-output performance pair (`fixed-cycle-comparison.json`).
Read-only inspection of the installed support GGUF confirms the confidence
head type8/Q8_0,4352 inputs. Correcting the activation quantization yields
confidence0.4063788 at that exact position and the original two draft IDs.
The stable Rust SDOT intrinsic is unavailable in this toolchain; the two SDOT
instructions are isolated in guarded Rust inline assembly, with stdlib NEON
FMA/reduction and the existing scalar fallback. No C host code or dependency
was added. Six focused DSpark CPU/layout tests pass, including the new
quantized-confidence test. Release all-target/all-feature build, Clippy with
warnings denied, rustfmt and diff checks pass after the correction.
Checkpoint-load invalidation now clears the new deferred capture and absolute
cache metadata through the same `reset_cache` helper as session reset. This
does not change the checkpoint format or disk KV policy.
### Completed functional comparison, performance still open
`quantized-rust.*` completes all three turns to natural EOS. All1755 returned
cycles match the original recording exactly, including warmup, IDs and committed
positions (`quantized-cycle-comparison.json`). All three text, thinking,
completion, prompt and cached-token comparisons pass
(`quantized-result-comparison.json`):
| Turn | Completion tokens | Prompt / cached | Rust / original prefill ms | Rust / original decode t/s | Decode gap |
| --- | ---: | ---: | ---: | ---: | ---: |
| Summary | 1208 | 2741 /1 | 5562.979 /5519.685 | 36.128 /36.580 | 1.24% |
| Story | 1455 | 3969 /3949 | 261.651 /281.080 | 30.366 /31.321 | 3.05% |
| Python | 347 | 5450 /5424 | 323.474 /344.868 | 33.936 /34.777 | 2.42% |
These are **diagnostic** timings with cycle logging, not repeat-median clean
performance acceptance. Story and Python still miss even the single-pair2%
threshold; the missing early-confidence gate, verifier submission/readback,
allocation and worker-lifetime tasks above remain explicit work. No UI canary
or real GUI event-loop acceptance is claimed for this series.
The Story has a coherent title, narrative and ending, not a clarification or
broken output. The Python answer is byte-identical to the previous original
DSpark answer (`python-reference-unchanged.json`), whose five generated asserts
and5011 independent cases passed in `deepseek-paired-20260911.nyUCsL/python-quality.jsonl`.
Six DSpark CPU/layout tests,17 shared sampling tests, the separate heap-fallback
test and two tokenizer tests pass; model-dependent tests are not silently
counted as run. The explicit32-cycle live regression passed before the final
confidence correction, and the final full1755-cycle harness/oracle comparison
validates the production path after it.
Final CLI SHA256:
`2d15458d12a14457519a22232061ce76182123b24f23d1ae595df424c36963e5`.
Traced standalone reference SHA256:
`395cc58f8ea59afa0a6e22f8d46b3fb3f129bc367b8ee3ee1a0dd4bc0f01bd4d`.
The working app bundle remains
`ea4d555c2faf0940d9cbcf76d8638ca614a9cb2c6b034e3b2f80aeef86b0b339`.
### Early confidence and lazy verifier output
The shared Rust executor now follows the original early confidence gate before
the draft vocabulary projection. Hidden and logits rows are read only when the
proposal loop needs them; disabling confidence keeps hidden/head encoding fused.
The ordinary verifier keeps its layer commands alive through the output head
and the existing GPU argmax/top-k reduction. It reads only the committed
distribution; exact stochastic sampling retains all row distributions.
No prefill-cap, power, artificial pause, kernel math or KV persistence change.
`lazy-head-rust.*`, `lazy-head-cycle-comparison.json` and
`lazy-head-result-comparison.json` establish the unchanged complete1755-cycle
recording and all three answers/thinking/token/cache/stop boundaries. Seven
focused CPU/layout tests pass (two installed-model tests remain ignored by
default), including the committed-row boundary regression. Release all-target,
all-feature build and warnings-denied Clippy pass. This full default-mode
comparison is not a new exact-stochastic oracle claim.
CLI SHA256: `de10b537c2cf2bfbb680ebc350e810305def3ca2ba59f0bea89f1d8c980699cb`.
The original driver and working app bundle hashes above are unchanged.
`run-clean-pairs.sh` records serial three-turn timing pairs without cycle logging,
original proposal logging or canary, with alternating reference-first/Rust-first
order. Both workers retain startup/progress/memory supervision, no total timeout.
The first postprocessing command used the wrong reference event name (`result`
instead of `reference_result`) and failed after both workers had finished; the
comparison was corrected against the unchanged receipts, with no inference rerun.
All six processes completed successfully, with natural EOS for all nine Rust
answers and matching reference content, thinking, prompt, cache and completion
counts (1208/1455/347 in every pair). `clean-{1,2,3}-comparison.json` and
`clean-pairs-summary.json` retain all results; none were discarded:
| Pair / order | Turn | Rust / DS4 prefill ms | Rust / DS4 decode t/s | Decode delta |
| --- | --- | ---: | ---: | ---: |
| 1 DS4→Rust | Summary | 6396.150 /5435.701 | 34.878 /37.298 | 6.49% |
| 1 | Story | 262.284 /289.008 | 28.623 /31.716 | 9.75% |
| 1 | Python | 350.909 /332.242 | 30.372 /35.511 | 14.47% |
| 2 Rust→DS4 | Summary | 7905.861 /7910.498 | 29.783 /28.784 | +3.47% |
| 2 | Story | 307.278 /376.104 | 25.918 /24.574 | +5.47% |
| 2 | Python | 363.522 /488.866 | 29.186 /27.959 | +4.39% |
| 3 DS4→Rust | Summary | 8389.686 /8292.276 | 27.523 /27.801 | 1.00% |
| 3 | Story | 301.627 /383.962 | 24.399 /24.151 | +1.02% |
| 3 | Python | 379.914 /458.934 | 27.995 /27.801 | +0.70% |
Pair3 is within2% in decode, with Summary prefill1.17% slower and the two short
continuation prefills faster. **It is not sufficient acceptance for this series:**
DS4 itself slows from37.30 to27.80 Summary t/s (about25.5%), and pair order changes
the sign of the relative gap. Median aggregation cannot establish a causal code
speedup in this nonstationary series. Do not compare the clean first pair against
the earlier logged pair as a before/after regression claim. Remaining worker and
allocation discrepancies still need their reference-aligned implementation and
an appropriately controlled follow-up.
Read-only system checks during pair2 reported no recorded thermal/performance
warning and25% system memory free; our benchmark processes ran strictly serially.
These observations do not prove stable GPU clocks or exclude other system GPU
activity. No application was stopped or configuration changed in response.
Rust supervisor totals were107.691/121.168/129.178s, maximum observed progress
gaps6.396/7.905/8.389s in prefill, supervisor lag60/59/59ms and exit0 throughout.
Canary was off: these are not GPU-canary or actual GUI event-loop measurements.
Original supervisor totals were96.236/124.665/127.935s, all with `error:null`.
The existing ignored `flash_0731_runs_exact_sampled_dspark` initially failed its
draft-count assertion: its four-token allowance takes the scheduler's existing
`max_tokens <10` no-draft branch. The test now allows16 tokens and explicitly
disables the confidence gate to exercise drafting independently of this short
fixture. It retains the original assertions and additionally invokes two-row
verification, checking both complete finite distributions, the GPU top ID
against CPU argmax, and the retained last-row logits. The supervised rerun
passes in1.60s (`lazy-head-exact-fixed.*`); the initial failure is retained in
`lazy-head-exact-test.*`. This is a regression check, not a new exact-sampling
reference/performance claim. No production setting was changed for this test.
Final warnings-denied Clippy, rustfmt and diff checks pass after this test-only
edit. No model or benchmark process remains from this work unit.
### Persistent CPU Markov workers
`src/engine/metal/markov.rs` replaces per-draft scoped thread creation with a
model-owned Rust worker pool. Original source contracts are `ds4.c:19121947`
(default min(online,12), caller plus helpers), `19712005` (contiguous partitions,
serial execution below512 rows), and `3385933963` (ordered first-tie Q8 argmax).
Positive integer `DS4_THREADS` settings are supported up to the original32-thread
limit. No CUDA/non-Apple Markov branch is enabled.
The GGUF mapping is shared through `Arc<Mmap>` without remapping or copying the
weights. Worker inputs own their temporary data: the full logits `Vec` is moved
into shared read-only ownership and returned after all dispatched jobs finish.
There are no borrowed raw pointers between worker lifetimes. Workers release
their input before signalling completion; results are drained on failure too,
and pool destruction closes and joins every helper. Existing dot/quantization
functions are reused without arithmetic changes. No dependency or native host
code was added.
`markov-rust.*` completes the full warmup and ongoing three-turn chat. All1755
original cycles and all answer/thinking/token/cache/EOS checks remain exact
(`markov-cycle-comparison.json`, `markov-result-comparison.json`). Two focused
Markov tests and seven GGUF tests pass, including thread and logits-buffer
reuse, ordered ties, shape validation and mapping bounds. Release all-target/
all-feature build and warnings-denied Clippy pass. The full diagnostic run is
not throughput acceptance. CLI SHA256:
`0e24c6e8102c2035cc89e93f77eede4235f5aa4611c36ce5fb225770641485f2`.
The remaining allocation task is concrete: `BatchScratch::allocate` reserves
workspace based on both row count and context position; `snapshot_spec_frontier`
allocates compressor/indexer/target snapshots every verifier cycle. Reuse must
preserve delayed support seeding, partial acceptance, error rollback and padded
output-head behavior. Merely retaining an arbitrary previous batch is not a
safe implementation of the original persistent scratch contract.
Two clean full pairs (`clean-markov-{1,2}-*`) ran DS4→Rust→Rust→DS4, power100,
same installed files/settings/warmup/ongoing chat, trace and canary off. All
content/thinking/prompt/cache/token checks pass; both workers in both pairs exit
successfully, without downloads or overlapping model processes:
| Pair | Turn | Rust / DS4 prefill ms | Rust / DS4 decode t/s | Decode delta |
| --- | --- | ---: | ---: | ---: |
| 1 | Summary | 6686.486 /5445.567 | 32.046 /36.968 | 13.32% |
| 1 | Story | 296.457 /280.371 | 26.201 /31.162 | 15.92% |
| 1 | Python | 376.741 /362.034 | 28.251 /34.122 | 17.21% |
| 2 | Summary | 8407.889 /8535.688 | 26.901 /27.488 | 2.14% |
| 2 | Story | 332.174 /383.535 | 23.301 /24.322 | 4.20% |
| 2 | Python | 398.755 /466.153 | 26.975 /28.215 | 4.40% |
The second pair still misses decode parity. DS4 Summary throughput again falls
by about25.6% over the series; no systemwide throttling cause is asserted. A
single `sudo -n powermetrics` query failed immediately because a password was
required (`markov-power-sample.txt`); it started no sampler. Rust supervisor
totals117.095/132.871s versus DS4 totals98.054/127.837s include different
frontend/finalization work and are not pure decode. Rust maximum progress gaps
were6.686/8.407s and supervisor lag59/58ms. No canary/UI-responsiveness claim.
To separate dispatch cost from GPU drift, the ignored CPU-only
`installed_markov_worker_dispatch` test uses installed Markov W1 row671 and the
actual W2 mapping with a fixed logits row. It compares per-call18-thread spawning
against persistent12 and18 workers, holding row arithmetic/input ownership
constant. Four alternating orders,128 calls per mode per round, all1536 argmax
results identical. Median times per128 calls:
| Dispatch | Median ms | ms/call |
| --- | ---: | ---: |
| Scoped18 | 40.356 | 0.3153 |
| Persistent12 (production default) | 36.077 | 0.2819 |
| Persistent18 (diagnostic only) | 30.496 | 0.2382 |
The default pool reduces this isolated dispatch/calculation time by10.6%, about
0.034ms per call; it does not explain seconds of whole-chat difference. This
diagnostic is neither a C Markov microbenchmark nor end-to-end parity evidence.
Production retains the reference's default12-thread policy. The same runnable
worker regression also closes one helper and checks that all other jobs drain,
an error is returned and the caller's original logits buffer is preserved.
Receipts: `markov-dispatch-test.*`, `markov-worker-failure-tests.txt`.
Final verification after the additional test-only coverage: seven enabled
DSpark tests pass; the explicitly supervised exact-sampling test passes in1.35s;
warnings-denied Clippy, rustfmt and diff checks pass. All processes from this
work unit have finished. No app bundle replacement, commit or push was made.
The full six-cell performance goal remains open.
### Persistent verifier buffers: functional checks pass, timing remains unaccepted
The next implementation reuses baseline compressor/indexer snapshots, high-water
prefix snapshots and one verifier workspace per existing padded row shape.
Deferred seed ownership determines when a batch can be recycled; full-prefill
workspaces are not retained. Position-sensitive verifier storage reserves the
session context. Existing output-head row padding, sampling, power and command
submission boundaries are unchanged. Error rollback keeps its previous behavior;
an error may drop scratch and require allocation on the next attempt.
Evidence remains in `local-eval-results/ds4-cycle-audit-20260911.vfsUc8/`.
CLI SHA256: `4f3643bd854cad1d317fdd6bb93649327902b19cb3cd7b9cfff24a760d57e880`.
The prior CLI is retained as `before-verifier-reuse-ds4-server` for a later
controlled comparison; merely comparing older sequential runs is insufficient.
- `reuse-cycle-test.txt`: the32-cycle original oracle passes, now also checking
native buffer identities for both padded verifier shapes and byte-exact
compressor/indexer rollback after another target step.
- `reuse-cycle-comparison.json` and `reuse-result-comparison.json`: the complete
traced chat matches all1755 original cycles and all text/thinking/token/cache
checks. Traced timing is diagnostic only.
- `reuse-exact-test.txt`: supervised exact-sampling regression passes after the
reuse changes, including complete target-distribution readback.
- `reuse-build.txt`, `reuse-clippy.txt`, `reuse-dspark-tests.txt`: release build,
warnings-denied Clippy and seven enabled DSpark tests pass.
- GPU allocation at each measured turn end is95,039,750,144bytes, approximately
73MiB more retained than the prior Summary endpoint. This is retained scratch,
not proof of a throughput improvement.
Two complete pairs ran in DS4→Rust→Rust→DS4 order, with trace/canary off and no
overlapping model processes or builds. The user subsequently confirmed concurrent
video playback; "clean" in these artifact filenames means instrumentation off,
**not** an idle GPU. Both pairs preserve all outputs and exit successfully.
| Pair | Turn | Rust / DS4 prefill ms | Rust / DS4 decode t/s | Decode delta |
| --- | --- | ---: | ---: | ---: |
| 1 | Summary | 6679.941 /5718.646 | 34.204 /38.273 | 10.63% |
| 1 | Story | 259.070 /273.257 | 28.306 /33.200 | 14.74% |
| 1 | Python | 355.567 /333.715 | 29.343 /36.744 | 20.14% |
| 2 | Summary | 8968.416 /10283.116 | 26.678 /24.830 | +7.44% |
| 2 | Story | 324.111 /445.256 | 21.926 /22.225 | 1.35% |
| 2 | Python | 365.097 /560.790 | 23.092 /26.814 | 13.88% |
Receipts: `clean-reuse-{1,2}-{comparison.json,rust.jsonl,reference.jsonl}` and
matching stderr. Rust supervisor totals110.153/140.256s versus original
93.671/141.037s include frontend/finalization differences, not just decode.
Rust progress gaps6.679/8.968s and supervisor lag45/45ms are not GPU-canary or
GUI-eventloop delays. No new responsiveness acceptance, app bundle replacement,
commit or push. Remaining work includes telemetry ownership reconciliation,
reference code/cost audit and the controlled full six-cell comparison.
### Interim checkpoint requested by the user
The current work unit is being closed for commit/push, not declared full parity.
The commit-gate test run exposed an already-committed stale Qwen source-inventory
assertion:22 runtime units were expected although both HEAD's Metal export and
the pinned generator contain26. The read-only command below verifies the entire
export byte for byte against pinned sources (17 custom bodies,26 runtime units,
dynamic QSA sources). Only the stale expected count is corrected; no kernel,
hash, fixture or per-body assertion is changed or removed.
```sh
python3 tools/mtplx-kernel-source.py local-eval-results/mtplx-reference-e652d55 --gated-delta-source local-eval-results/mtplx-reference-env-0.32.2/lib/python3.12/site-packages/mlx_lm/models/gated_delta.py --check
```
The app bundle is rebuilt as required by the commit gates. This supersedes the
earlier statements that this follow-up had not yet replaced the bundle; no GUI
or new throughput series is launched. Unrelated `tools/__pycache__/` files are
left untouched and excluded from the commit.
Commit verification: `cargo fmt --all -- --check`, warnings-denied all-target/
all-feature Clippy, `make bundle`, and `cargo test --all-features` pass. The full
suite reports303 main tests plus4 supervisor and4 integration tests passed,
zero failures,202 explicitly ignored main tests. The separately supervised
32-cycle and exact-sampling results above remain distinct from these normal
gates. `codesign --verify --deep --strict` and the bundle's `model-eval --help`
also pass without opening the GUI. Final suite receipt:
`local-eval-results/ds4-cycle-audit-20260911.vfsUc8/interim-commit-tests.txt`.
+585
View File
@@ -0,0 +1,585 @@
# GLM execution and responsiveness follow-up — 2026-09-11
## User acceptance and scope
- Qwen is confirmed good in normal interactive use.
- GLM decode is now also confirmed good interactively. GLM prefill remains
usable, but feels less smooth than Qwen and affects other applications.
This is not a claim of a complete freeze or a new confirmed beachball.
- The user authorized resuming the outstanding work and explicitly authorized
compiling/running antirez/ds4 as a standalone, supervised reference benchmark.
No DS4 C objects are linked into DS4Server or its application bundle.
- Qwen's golden master remains MTPLX; DeepSeek/GLM remain antirez/ds4.
The accepted 2.6% Qwen Summary AR exception is not a general tolerance.
## Implemented execution changes
Both scalar GLM loops now flush periodically every four completed layers,
excluding the final layer and SSD expert streaming. Previously they flushed
only once at layer four. This follows the active indexed DS4 graph, including
scalar MTP fallback/rejection replay. The reference's dynamic per-layer mapping
fallback must not be confused with Rust's static non-expert decode mapping:
`glm_streaming_model_spans` retains non-expert tensors, the configured resident
expert prefix, and incompatible expert layouts; selected experts are loaded
through the existing native cache. No new per-layer SSD waits were introduced.
Low-memory dynamic mapping fallback parity is not established by this patch.
GLM 5.3 prefill progress now advances at existing completed GPU drains and after
the final output evaluation, not after every submitted layer. Chunk selection,
prefill flush/drain placement, Metal kernels, sampling and power policy are
unchanged. This corrects progress accounting; it does not by itself fix the
remaining prefill smoothness issue.
Targeted checks passed: periodic/final/SSD decode boundary test; existing
prefill boundary test; live two-row verifier acceptance, rejection, rewind,
scalar fallback, recurrent state, unused HC workspace guards and lifetime
counters. The live test additionally verifies completed-prefill progress points.
## Standalone reference and instrumentation
`tools/ds4-session-reference.rs` is a separate Rust benchmark driver for the
unchanged public DS4 engine/session interface. It is deliberately not a Cargo
target and is never included in the app. Its build script verifies reference
commit `ec7642cdd9ec81d01ad4b1fd8f8a3d1511533748`, the pinned header hash,
unchanged tracked engine sources and current reference objects. The arm64 ABI
layout is checked against Clang's record layout (engine options 280 bytes,
distributed offset 152, TP offset 216).
The public DS4 CLI/API maps `low` to `high`. The driver instead constructs the
Low system prefix through the public chat API and passes tokens to the original
session implementation. It also reproduces the UI's separate system-prefix
prefill (9 tokens for GLM), and retains generated token history in one session.
`glm53_reference_prompt_tokens_match_shared_runtime` checks every token of all
four prompt streams, including continued turns, against the production Rust
tokenizer. The first complete matched-bootstrap AR reference passed this check.
Build from the DS4Server checkout, with already-built reference objects:
```sh
bash tools/build-ds4-session-reference.sh /Users/gb/Projects/ds4 /absolute/path/reference
```
Run the resulting binary from the reference checkout under `test-supervisor`:
```text
test-supervisor 114688 30 45 --command /absolute/path/reference MODEL_GGUF glm on README_PATH
```
The driver uses power100, Low, context32768, temperature0.6, top-p0.95,
top-k0, min-p0, seed42, SSD streaming off and graph-selected GLM chunks.
Warmup is a separate session (up to32 tokens), followed by README Summary,
lighthouse Story, and Python `is_prime` in one ongoing chat, to natural EOS.
There is no total-runtime watchdog. Missing models fail; no downloads occur.
DeepSeek is supported by the driver's `deepseek` family argument, with the
installed DSpark support GGUF required for acceleration-on; it has not yet been
validated by this follow-up's GLM runs.
Optional `DS4_REFERENCE_CANARY=/absolute/path/ds4-server` starts the **same native
probe and monitor implementation** as the UI/harness in a separate process.
The `gpu-canary` CLI accepts phase labels on stdin and ends on EOF. A readiness
handshake waits for the first successful probe before model loading. Its
readiness/stall/sample reports are not model-progress watchdog heartbeats.
Clean throughput runs leave this variable unset. An external observer and an
in-process observer must not be treated as identical OS scheduling conditions.
`DS4_REFERENCE_IN_PROCESS_CANARY=1` instead enables a native probe thread inside
the reference process. It links the same `native/metal/ds4_canary.m` used by
DS4Server, with the same4096-byte blit, separate queue and100ms cadence, rather
than duplicating a Metal implementation. This bridge contains no model code.
Its Rust monitor logs per-sample phases/timing but is not the UI event loop.
The optional external observer can also be enabled simultaneously. Neither is
enabled for clean reference throughput. `reference --canary-self-test` exercises
readiness, two phases and clean shutdown without loading a model.
The initial external-probe integration test exposed a startup race: a phase
could end before the executable initialized. It was not worked around by
loosening the assertion; the adapter now requires a readiness handshake. The
initial diagnostic without that handshake is retained, not a full-startup proof.
## Measurements and limitations
All raw receipts, outputs and failed attempts are retained under
`local-eval-results/glm-scheduling-20260911.7VSaeW/` (ignored, local evidence).
The before binary is the secured `02db096` implementation, SHA256
`ab289f067a7a01c22113eec76aa896638d83e392242192e1440d14ed11524d5c`.
Fresh complete AR runs (after, then before), same output/reasoning/tokens/EOS:
| Turn | Tokens | Before decode t/s | After decode t/s | Before prefill ms | After prefill ms |
| --- | ---: | ---: | ---: | ---: | ---: |
| Summary |640|24.467|25.403|7561|6034|
| Story |1102|23.703|24.135|262|255|
| Python |200|24.726|25.401|305|302|
These are single sequential pairs, not drift-controlled medians. The large
summary-prefill difference cannot be attributed to a decode-only flush change.
No hard decode regression was observed; full performance parity is not proven.
The matched-bootstrap original DS4 AR reference completed naturally at
26.618/25.123/25.825 decode t/s, with779/1047/198 output tokens. Its generated
text differs from Rust despite matching initial prompt tokens/settings; later
contexts therefore also differ. This is not an exact-output performance pair.
Reference prefill timers measure session sync; Rust's current GLM `prefill_ms`
still includes the observed UI phase. Do not silently equate those intervals.
The new Rust MTP run retains the previous625/1026/196 completion tokens and
identical output/reasoning. Its draft acceptance fractions are241/385 (62.6%),
342/685 (49.9%) and95/102 (93.1%). Python therefore has the expected higher
acceptance; MTP's benefit is workload-dependent, not uniformly absent.
The fresh MTP before/after pair also preserves every output/reasoning token
and natural completion:
| Turn | Tokens | Before decode t/s | After decode t/s | Before prefill ms | After prefill ms |
| --- | ---: | ---: | ---: | ---: | ---: |
| Summary |625|19.988|23.154|9303|6190|
| Story |1026|17.728|19.295|306|272|
| Python |196|27.843|29.506|347|322|
The same sequential-run/drift limitation applies. This establishes no observed
hard regression, not a controlled causal speedup or reference-parity acceptance.
### Canary placement and timestamp attribution
The full `after-mtp-dual-canary` run had simultaneous internal/external probes.
The internal probe recorded867 successful samples, with a prefill maximum
of490.740ms and decode maximum3.180ms. The external probe was ready before model
launch and continued until after termination:3004 successful samples, overall
maximum3.331ms (startup), and1.572ms while labelled `preparing` across the model
lifetime. That external label is deliberately not turn/phase attribution.
Both probes stopped cleanly; no sample failed or reached2s. This is diagnostic
evidence, not a clean throughput run or a compositor-frame test.
Thus the previous `completed_ms` cannot be interpreted as a measured systemwide
GPU blockade. It includes host-side waiting and completion delivery. The optional
shared native probe now also records commit-to-GPU-start (`gpu_wait_ms`),
GPU-start-to-end (`gpu_interval_ms`), and GPU-end-to-host-return (`host_return_ms`).
Metal's GPU timestamps use system mach time; the probe uses `mach_absolute_time`
and the native timebase for those differences, not `CLOCK_MONOTONIC`. Missing or
inconsistent timestamps remain null, not zero. The GPU interval includes possible
GPU scheduling/preemption, not exclusively active blit execution. See Apple's
[GPUStartTime documentation](https://developer.apple.com/documentation/metal/mtlcommandbuffer/gpustarttime).
The model-free Metal integration check verifies phase coverage, valid nonnegative
intervals and their bounds against wall completion; the synthetic unit check
retains null timing when unavailable. The existing UI uses the same enhanced
native probe, but its stats panel still displays the existing wall latency fields.
Neither model work nor disabled-canary execution invokes the new timestamp work.
The first full Rust timestamp run (`after-mtp-timeline`) preserved every MTP
output/reasoning token and EOS. All902 samples had valid Metal timestamps and
none failed. Its worst prefill sample was300.003ms:299.892ms before GPU start,
0.001917ms GPU interval, and0.105958ms after GPU end. The decode maximum was
3.733ms. This directly rules out delayed host return as the dominant cause of
that prefill sample; the queued probe waits for GPU execution. It does not show
that a different application's rendering queue is delayed by the same amount.
The subsequent extraction into the shared native object changes no probe work:
direct `[cb commit]` replaces the wrapper whose model-queue-only hook never
applied to this separate canary queue. Both native bindings pass their model-free
checks after extraction. Final current product binary SHA256:
`50b4b4abbbdc45ff600c1f46d0bec611879249ac8e4d8291d22d656b9c6e9a5d`;
standalone reference binary:
`ccc7a8a774cb1c202add6dba60b04dffe3597822b15a34e22c7e4a5574b50adf`;
shared probe source:
`dd3abc34088ee27ba0759f01a291b9b714114420295252d63e85fd6f326fddab`.
Answer correctness is checked separately from natural termination. Rust AR,
Rust MTP and reference AR passed their generated assertions plus5011 `is_prime`
cases (-10 through5000). The preliminary reference MTP output passed its own
five assertions but failed347 additional cases, first at49: it omits the
`i + 2` divisor test. This is a failed generated Python answer, not by itself
evidence of an engine defect. It must not be reported as a successful code
benchmark merely because EOS was reached. Details are in `python-check.json`.
### Reference clean MTP and record integrity
The clean `reference-mtp-clean` run (both canaries disabled) completed all turns
with the same593/872/167 tokens, text, stop tokens and failed Python answer as
the diagnostic reference run. Its prefill times were5462.306/351.803/434.414ms;
decode21.483/16.754/26.143t/s. The preceding in-process diagnostic measured
23.623/19.287/30.440t/s. This spread must not be disguised as a port speedup or
accepted2% parity: it is one sequential comparison with different probe state,
not controlled repeated clean medians. Canary-on throughput is not the baseline.
The first internal reference run reported788 successful probes, but only787
were independently parseable: a watchdog resource record interrupted one
canary JSON record at a pipe-read boundary. That failed record is preserved in
`reference-mtp-inline.stderr.log`, not silently counted as missing/zero latency.
The supervisor now forwards complete lines in one locked stream write (with a
64KiB cap for newline-free output), while watchdog progress still consumes every
incoming chunk immediately. EOF flushes partial output. A split-record regression
test and all existing memory/start/continuation/long-run watchdog tests pass.
The reference diagnostic is repeated as `reference-mtp-inline-records` for a
fully parseable receipt; the earlier run is retained as the failure evidence.
That repeated reference run completed with **771/771 parseable, successful,
fully timestamped samples** and identical593/872/167 generated tokens/text/EOS.
Prefill p95/max was233.106/264.292ms (48 samples); decode p95/max was
0.226/19.338ms (713 samples). The worst prefill probe waited264.195ms before
GPU start, ran over0.001750ms, and returned to the host0.092083ms after GPU end.
No probe reached2s. The reference's prefill samples also include its short
warmup; the worst sample occurred during the measured summary prefill.
Its diagnostic throughput was23.893/19.177/32.090t/s, not the clean baseline.
| Matched native in-process probe | Prefill p95 ms | Prefill max ms | Decode max ms |
| --- | ---: | ---: | ---: |
| DS4Server, `after-mtp-timeline` |289.978|300.003|3.733|
| Original DS4, `reference-mtp-inline-records` |233.106|264.292|19.338|
These sequential diagnostics reproduce the same GPU-start-wait phenomenon in
the golden master. They do not excuse the remaining Rust prefill cost, establish
statistical latency equivalence, or measure another application's compositor.
Moving inference to another thread cannot by itself reproduce the independent
process's scheduling conditions; process isolation is a distinct architectural
option, not implemented or declared proven as a UI fix here.
Verification at this checkpoint: release all-target/all-feature build; release
all-target/all-feature Clippy with warnings denied; rustfmt and diff checks;
11 model-eval unit tests; both model-free native probe bindings; all4 supervisor
tests; earlier live GLM verifier/progress/HC guards and the full AR/MTP chats.
The updated supervisor fixes measurement transport, not inference scheduling.
## Sampling versus model execution — continued investigation
The prior follow-up made concrete progress (execution fixes plus a fair native
in-process latency reference), but did not establish the full three-model,
AR/speculative2% goal. This continuation addresses the different GLM outputs
before treating their different ongoing histories as matched performance work.
`reference --sampler-fixture` runs the original public `ds4_sample_logits` without
loading a model or using Metal. The checked-in
`tests/fixtures/ds4-sampling-ec7642c.json` contains64 cases: four vocabulary sizes,
eight temperature/top-k/top-p/min-p settings, seeds0/42,32 consecutive tokens
per case and the final RNG state. The original Rust test failed40 of64 cases.
The shared DS4/GLM sampler now preserves the first argmax tie and original
negative sentinel, skips RNG consumption for greedy/all-invalid and the DS4
full-vocabulary min-p fallback, and preserves seed0 until the original RNG's
zero-state substitution. Qwen's independent MTPLX sampler is untouched.
All64 oracle cases and the16 enabled sampling tests pass. Crucially, the positive
temperature/top-p benchmark cases at seed42 already passed before the fix:
these edge corrections are not the explanation for the observed GLM chat gap.
Optional `DS4_REFERENCE_LOGITS_TRACE` records the first32 summary logit rows
through the public original session API. It requires AR mode, creates a new
file rather than overwriting one, and does not change generated tokens or RNG.
The full `reference-ar-logits` chat retained exactly the779/1047/198 tokens,
text and stop tokens of `reference-ar-bootstrap`. Its timings are diagnostic,
not a clean performance baseline. The binary trace contains19,824,640 bytes
(32 rows of154,880 little-endian floats). Its path is serialized as an OsString
and decoded losslessly by the replay test.
`glm53_reference_logits_replay_separates_sampling_from_execution` first samples
those original C-produced rows through the production Rust sampler: **all32
tokens match**. It then opens the installed GLM at Power100/context32768,
prefills the same9-token bootstrap and exact summary suffix, and advances only
with reference-selected tokens. Thus histories never diverge during comparison.
On the Rust-generated rows the test **fails at step17**, choosing906 instead of
the reference320. Already the first post-prefill row has max absolute difference
5.722162 and RMS difference0.851167. All32 per-step row errors are retained in
`logits-replay.stderr.log`; the watched test terminates normally with failure
status in9s. This is a new, deliberately retained red parity test, not a passed
live validation or a speed result. No DS4/GLM/Metal/CPU diagnostic override was
present in the parent environment.
The next localization belongs in the model execution path: compare existing
original DS4 per-layer tensor dumps with the corresponding Rust HC/KDA/DSA/FFN
stages, starting at the first bootstrap/prefill block. Do not explain this away
as stochastic output variation or hide it with a lower chunk/power setting.
No speculative numerical tolerance or new scheduling workaround was applied.
## Root cause: GLM 5.2 chunk boundary applied to GLM 5.3
The active original indexed GLM 5.3 path deliberately keeps full2048-token
chunks across both the old2048 indexer threshold and the4096/8192 dense-attention
threshold. Rust was still applying the GLM 5.2 top-k boundary: after the9-token
bootstrap it evaluated2039 tokens, whereas DS4 evaluated2048. This changes the
recurrent prefill computation, despite identical total prompt tokens.
The original layer0 bootstrap `attn_out` and `ffn_out` dumps matched Rust
bit-for-bit. The original position9 dumps contain2048*4096 floats, establishing
the actual chunk geometry rather than inferring it from configuration.
Detailed HC dump hooks elsewhere in DS4 belong to an inactive dense path and
were not used as evidence for the active indexed execution.
Rust now retains complete GLM 5.3 chunks and splits only the attention slices
at the dense/sparse boundary, as DS4 does. This also removes the incorrect
whole-pair sparse override for a two-row verifier crossing that boundary.
GLM 5.2 retains its old top-k splitting. Unit checks cover both families and
the4096/8192 attention transitions. No smaller chunk, delay, or power reduction
was introduced.
After this correction, the same fixed-history replay is green: **all32 full
154880-value logit rows are bit-identical** to the original trace (max absolute
and RMS error both0), and all sampled tokens agree. This run had no stage
instrumentation enabled. Evidence is retained under
`local-eval-results/glm-stage-20260911.rwQBaJ/mixed-replay.*.log`.
The earlier red replay remains historical evidence, not the current result.
The optional Rust stage reader exists only under `cfg(test)` and validates
tensor geometry before comparing values; it adds no production GPU drains.
The live verifier at frontier4095/context32768 passed across the4096 boundary,
including acceptance, rejection, rewind to either retained frontier, scalar
fallback and recurrent-state restoration (`mixed-boundary.*.log`,70.64s).
The32-row replay alone is not a complete performance or output-parity claim.
The initial source-only note about one-token suffixes was incomplete: the
shared UI/headless consumer already routes one-token extensions through scalar
execution. The actual remaining crossover was two/three-token extensions;
see the subsequent common-consumer correction below.
### Complete chats after the chunk correction
Fresh clean runs used the same ongoing workload, Power100/Low, native EOS,
separate warmup and no active canary. Rust executable SHA256:
`4b23c04325c931854b98c23bd2c98df8a5c2362927aa9b1faed65019d07fd40d`.
The original reference retained its prior tokens/text/stops exactly.
| Mode / turn | Rust tokens | DS4 tokens | Rust decode t/s | DS4 decode t/s | Output + thinking identical |
| --- | ---: | ---: | ---: | ---: | --- |
| AR Summary |779|779|24.512|20.955|yes|
| AR Story |1047|1047|22.956|19.744|yes|
| AR Python |198|198|23.436|20.650|yes|
| MTP Summary |593|593|20.235|21.427|yes|
| MTP Story |905|872|18.039|18.007|no|
| MTP Python |169|167|29.443|29.511|no|
All six Rust turns and six reference turns ended naturally. AR prompt/cached
counts also match exactly. Receipts: `clean-comparison.json`,
`mixed-ar-output-check.json`, `mixed-mtp-output-check.json` in the stage evidence
directory. The AR reference was materially slower than earlier clean runs;
these sequential pairs are not a controlled speedup or2% acceptance claim.
MTP Summary is about5.6% slower in Rust in this pair; the later MTP throughput
numbers do not compare identical histories. Prefill UI-phase and original
session-sync timers still have different boundaries (raw values in the receipt).
Control-loop maxima of4059ms are not GPU canary or compositor measurements.
### Second root cause: MTP stop token retained in the ongoing frontier
Although MTP Summary text/thinking and593 emitted tokens match, Rust starts
Story with3229 cached tokens and3249 prompt tokens; DS4 uses3228/3248.
The shared Rust generation consumer returned on an MTP stop token without
rewinding the already evaluated block. Both normal and raw original DS4 agent
consumers call `ds4_session_rewind(block_start + ti)` at that point. The standalone
reference's stop handling therefore agrees with its real agent, not just an
arbitrary benchmark convention.
The shared UI/headless consumer now calls `rewind_speculative_output`, a thin
GLM adapter over the existing two-row rollback, to keep exactly
`prompt_tokens + emitted_tokens` before retaining the chat.
This restores the saved two-row KDA state and replays the retained row; it does
not merely truncate IDs or re-render generated text. Invalid frontiers fail
explicitly. Both sampled and greedy generation use this consumer. Qwen's own
whole-turn controller is unchanged. Other model-specific speculative stop
contracts are not claimed validated by this GLM change.
The live verifier regression now exercises that same consumer rollback path.
`align_prompt` is intentionally not used: it retains one fewer token to force
logit recomputation during prompt synchronization, which is a different contract.
The full post-frontier-fix MTP measurement (`frontier-mtp.*.log`) now matches
the original for **all three turns**: text, thinking, emitted token count,
prompt count, cached frontier and natural stop. Emitted counts are593/872/167;
Story starts at3228 cached/3248 prompt tokens, Python at4120/4145. The executable
SHA256 is `b964336d64fbb90b3a9ca595a4705eda02e7afe9c39aedb4ea775e0d52fcf20e`.
`frontier-mtp-output-check.json` has three entries with every equality true;
the checked `jq -e` assertion requires all three entries and all five properties.
Decode rates are23.880/19.357/31.643t/s, versus21.427/18.007/29.511 in the directly
preceding clean original MTP run. This is one sequential pair, not repeated2%
acceptance. The Python answer is now exactly the reference's previously checked
incorrect answer (first counterexample49); matching the oracle does not waive
the independent generated-code quality failure.
### Final regression and responsiveness diagnostics
The final strict replay passes with bit-equal logits at all32 steps. The two
original layer0/position9 stage tensors each contain8388608 floats and also
match bit-for-bit (`final-replay.*.log`,10.21s). The updated live verifier at4095
passes through the same rollback entrypoint used by the consumer, including
invalid/unchanged-frontier checks, rejection and both retained rows
(`final-boundary.*.log`,82.27s). The five enabled GLM unit tests pass.
`final-canary` retained identical full MTP output/frontiers. Its in-memory
summary reports840 samples, no failures, prefill p95/max395.611/483.166ms,
decode max3.820ms and no sample crossing the configured2s threshold. However,
strict raw-log parsing found an interleaved canary/resource JSON record: the
model-eval parent inherited the child's stderr, and both processes serialized
JSON fragments to that descriptor. This raw file is retained as a **failed
record-integrity diagnostic**, not silently filtered into a complete sample set.
The model-eval supervisor now pipes child stderr and forwards complete lines
under the parent's shared stderr lock, the same lock used by resource samples.
Diagnostics do not refresh inference progress deadlines. Reader failures are
reported on join. This fixes the app harness counterpart of the earlier
standalone watchdog forwarding issue; it changes measurement transport, not
GPU scheduling or the UI inference graph.
The directly following original DS4 in-process probe run
(`final-reference-canary`) has841/841 parseable samples, no failures, unchanged
reference tokens/text/stops, prefill p95/max373.220/388.090ms and decode max4.370ms.
The worst prefill sample spent387.964ms before GPU start,0.002875ms over its
GPU interval and0.122ms returning to the host. Thus substantial prefill queue
waiting still occurs in the original oracle; the larger Rust spike is not
declared equivalent or explained away.
The repeated Rust run after the forwarding correction (`final-canary-records`)
completed the entire chat in83.744s and preserved all output/frontier fields.
Every JSON record beginning with `{` in stderr was parsed with `fromjson`
(no error suppression): **776/776 canary records and82/82 resource records**
match the independently reported totals. The checked receipt is
`final-canary-records-check.json`. There are no probe failures or observed2s
threshold crossings. Prefill p95/max is119.507/247.181ms; decode max1.854ms.
The worst sample waits247.062ms before GPU start, spans0.001750ms on the GPU,
and returns after0.115458ms. This lower maximum is not attributed to the
transport-only fix: the prior483ms Rust and388ms original spikes remain recorded,
and scheduling/throughput variability still requires repeated paired testing.
The optional probe remains off by default; no negligible-overhead claim is made.
Final source verification: release all-target/all-feature build and Clippy
with warnings denied; rustfmt/diff checks; five GLM unit tests; eleven
model-eval unit tests; sixteen sampling tests including the64-case original
sampler fixture; strict live logits/stage and consumer-rollback boundary tests.
The final CLI SHA256 is
`cbe04f8ce8f8d2fcb6c82b97c3d85b7bed561418893621a6a653d344d1aa6d85`.
The previously good bundle remains unchanged at SHA256
`ea4d555c2faf0940d9cbcf76d8638ca614a9cb2c6b034e3b2f80aeef86b0b339`.
## Common prompt timing and DS4 CPU sampling follow-up
Evidence for this continuation is under
`local-eval-results/glm-paired-20260911.eClfCS/`. The preceding goal turn made
verified progress (chunk scheduling and stop-token frontier fixes); it did not
establish the full six-cell performance goal.
The shared consumer now uses DS4's GLM5.3 resumed-prefill crossover of2 tokens,
not the generic4-token threshold. DS4 explicitly documents this choice as
measured on M5 Max/GB10 (`ds4.c:36784`). One-token continuations were already
scalar; cold/vision paths and the separate MTPLX whole-turn controller are
unchanged. The enabled crossover test covers GLM5.3 versus GLM5.2/DeepSeek.
DeepSeek/GLM now publish the existing `PromptTiming` at the shared prompt-
evaluation boundary: after restoration/bootstrap, around actual suffix execution
including its progress callbacks, before decode/checkpoint storage. Exact cache
hits report zero evaluated work. Separately unmeasured restore/history components
are `null`, not fabricated zeros; Qwen continues reporting the same measured
numeric values through `Some`. The new metric test and existing Qwen progress/
decode-timer test pass. The ordinary UI-prefill timer remains separately visible.
A fresh clean AR pair kept all three outputs/thinking/token counts/frontiers
identical. Rust's engine-prefill times were5235.183/269.769/320.767ms, original
DS4 session-sync8160.750/495.857/479.216ms; Rust decode24.811/23.114/23.410t/s
versus16.633/17.333/19.054. These large sequential-run differences are not a
controlled speedup or a completed repeat matrix (`baseline-ar-comparison.json`).
The reference driver now additionally queries and checks actual engine power100
after load, rather than only recording its requested options.
The CPU sampler still differed algorithmically: Rust sorted the full vocabulary
and drew from renormalized probabilities, while DS4 first tries a512-candidate
heap and draws from raw retained weights. A CPU-only replay uses the existing32
full logit rows, one32-draw warmup and16 measured batches (512 draws). The same
small runner serves the independent original public `ds4_sample_logits` and the
production Rust sampler. It loads no model and performs no Metal work; both are
supervised with1GiB memory/start30s/idle30s limits. The original public function
allocates a scratch buffer per call, unlike its session API, so its microbenchmark
is not an exact measure of session-sampler overhead.
Before alignment Rust took2.645ms/draw versus original0.834ms, with all512 tokens
equal. The aligned Rust path initially measured0.401ms/draw with the same512
tokens (`sampler-{before,after}-rust.json`, `sampler-reference.stdout.log`).
It uses stdlib `BinaryHeap`, DS4's logit/index tie order, bounded-nucleus fallback
without advancing RNG, original raw cumulative sampling, full-vocabulary/min-p
fallback and the original expf-verified log-space rejection boundary. Top-k
retains the original1024 cap. Separate distribution materialization for
speculative correction and Qwen's MTPLX sampler are untouched.
All64 original sampler fixture cases and17 enabled sampling tests pass, as does
the added missing-mass/near-one fallback, RNG and signed-zero tie check. Release
all-target/all-feature build and warnings-denied Clippy pass. The new executable
SHA256 is `ece6aed3601fb402e6dba6ac2e289d6e0c2dc86663600c3d4b1a4cc07e8fb42c`.
The first full post-sampler AR and MTP pairs both preserve all three outputs,
thinking, completion/prompt/cached counts and natural stops. The independently
queried reference engine reports power100. Receipts are
`sampler-{ar,mtp}-comparison.json`; these are single pairs, not the repeat matrix.
| Mode / turn | Rust / original engine-prefill ms | Rust / original decode t/s |
| --- | ---: | ---: |
| AR Summary | 5208.845 / 5548.775 | 26.129 / 24.711 |
| AR Story | 267.263 / 287.268 | 24.553 / 23.728 |
| AR Python | 319.127 / 341.355 | 24.964 / 24.520 |
| MTP Summary | 6574.400 / 5427.866 | 23.935 / 23.628 |
| MTP Story | 273.240 / 278.956 | 19.419 / 19.089 |
| MTP Python | 315.279 / 360.621 | 33.140 / 31.919 |
The Summary MTP prefill regression in this pair remains visible despite the
slightly faster Rust decode. Reversed-order repetitions are needed to distinguish
run variability from a repeatable graph cost. AR before/after the sampler keeps
the entire chat output identical and improves decode by5.310/6.225/6.638% in this
one sequential comparison (`sampler-ar-before-after.json`); no controlled causal
end-to-end percentage is inferred from that pair alone.
MTP is not universally beneficial in the original either: its Story decode is
19.089t/s versus23.728 AR, while Python is31.919 versus24.520. Rust's full MTP
cycle receipts show228/366,289/584 and82/86 accepted drafts respectively
(62.3%,49.5%,95.3%). The corresponding complete decode-loop time per cycle is
67.69/76.89/58.60ms. At1.62/1.49/1.94 emitted tokens per cycle, the Python case
amortizes the extra draft/verification work much better. These are whole-cycle
averages, not isolated kernel timings: the existing `verifier_ms` includes other
cycle work and must not be presented as an exclusive verification stage.
AR and MTP have different natural histories, so their t/s comparison is not a
matched-token microbenchmark. The previously recorded Python correctness failure
also remains open even though both implementations produce the same code.
### Reversed-order pairs: acceptance still fails
Both modes were repeated in original-then-Rust order, serially without builds
or canary probes. All twelve measured answers in these four processes again
match text/thinking/counts/cache frontiers and end naturally; all watchdogs
exit successfully. No slow run was discarded (`repeat2-*-comparison.json`).
| Mode / turn | Rust / original engine-prefill ms | Rust / original decode t/s |
| --- | ---: | ---: |
| AR Summary | 6738.280 / 5283.041 | 23.569 / 25.535 |
| AR Story | 309.647 / 281.591 | 21.743 / 24.186 |
| AR Python | 387.381 / 329.667 | 21.107 / 24.898 |
| MTP Summary | 9240.178 / 8835.102 | 17.706 / 17.678 |
| MTP Story | 358.905 / 402.447 | 15.146 / 14.581 |
| MTP Python | 403.605 / 491.547 | 25.843 / 23.655 |
AR decode now misses by7.70/10.10/15.23%; MTP Summary prefill misses by4.38%.
The subsequent original MTP run is itself much slower than its first run.
This excludes neither a Rust scheduling difference nor changing device clocks;
it does preclude a pass based on the favorable first pair or a selected median.
The required third pair and full six-cell acceptance remain outstanding.
Rust AR emits exactly9445/12612/2424 command buffers in both repetitions, with
the same outputs, but its GPU timestamp-interval sums increase from
34536/42061/8084ms to39285/47601/9590ms (`ar-drift-comparison.json`). Those sums
are `GPUEndTime - GPUStartTime` and may include preemption; they are not exclusive
kernel or hardware-clock measurements. The slowdown is not explained by changed
token counts or extra command buffers, and is not declared thermal throttling.
During the sequence, a read-only process snapshot showed only the intended
reference model process. macOS reported no recorded thermal/performance warning
and normal VM pressure (1), which does not exclude frequency changes. The
AGX PerformanceStatistics snapshot exposes utilization but no frequency field.
Hardware was freshly checked: Apple M5 Max,128GiB,18 logical CPUs.
## Remaining acceptance
- Compare repeated clean throughput pairs; GLM ongoing histories now match in
both modes, but sequential run variability does not establish2% performance parity.
- Localize the remaining GLM prefill cost against original DS4's active indexed
path, now that in-process GPU-start waiting is observable on both sides.
The engine-prefill timer is now exposed separately from UI-phase timing;
use that aligned boundary in the paired comparisons.
No chunk reduction or extra waits are justified by these measurements alone.
- Verify the corrected short-extension crossover live where needed, and other
model-specific speculative stop contracts; the recorded GLM workload does not
cover every possible interaction. Full chats pass after CPU-sampler alignment;
repeated timing acceptance remains separate.
- Validate the remaining SSD expert-streaming cases separately from resident
scheduling. This is unrelated to replacing DS4 KV checkpoint persistence.
- Complete the DeepSeek AR/DSpark reference cells and Qwen residual performance
analysis. Interactive confirmations are not a substitute for the six-cell
numerical acceptance matrix.
No bundle replacement, commit or push has been performed by this follow-up so far.
All processes have terminated. The subsequent DeepSeek comparison and its
separate bootstrap/DSpark findings are recorded in
[DeepSeek follow-up](deepseek-reference-followup-20260911.md).
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
implementation,mode,run,prompt_tokens,decode_tokens,prefill_tps,first_token_ms,steady_tps,p50_ms,p95_ms,cache_entries,cache_hits,cache_misses,pread_bytes,token_sha256
ds4server-baseline-79468c65,resident,1,16,128,69.627264,26.270667,39.534586,24.875959,26.607167,0,0,0,0,
ds4server-baseline-79468c65,resident,2,16,128,69.799133,26.365458,39.505867,24.905750,26.624667,0,0,0,0,
ds4server-baseline-79468c65,resident,3,16,128,69.618099,26.454750,39.526849,24.877584,26.585875,0,0,0,0,
ds4server-baseline-79468c65,ssd,1,16,128,6.824429,122.398208,8.638560,114.928000,124.375958,4096,34040,7208,51017416704,
ds4server-baseline-79468c65,ssd,2,16,128,6.805883,126.033250,8.634659,114.667833,124.220583,4096,34040,7208,51017416704,
ds4server-baseline-79468c65,ssd,3,16,128,6.726701,125.304417,8.532966,116.431375,124.278709,4096,34040,7208,51017416704,
ds4-8db89fe0,resident,1,16,128,56.49,22.706,45.67,21.905,22.297,0,0,0,0,ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
ds4-8db89fe0,resident,2,16,128,56.49,22.719,45.59,21.908,22.320,0,0,0,0,ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
ds4-8db89fe0,resident,3,16,128,56.51,22.638,45.60,21.890,22.342,0,0,0,0,ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
ds4-8db89fe0,ssd,1,16,128,12.07,46.746,25.73,36.461,46.861,4096,32958,4194,29684662272,2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
ds4-8db89fe0,ssd,2,16,128,11.88,46.737,26.11,35.622,46.982,4096,32958,4194,29684662272,2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
ds4-8db89fe0,ssd,3,16,128,11.16,47.092,26.20,35.513,46.554,4096,32958,4194,29684662272,2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
ds4server-issue-78,resident,1,16,128,69.930426,22.547583,45.683038,21.889833,22.275042,0,0,0,0,ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
ds4server-issue-78,resident,2,16,128,69.760230,22.657125,45.602401,21.936042,22.331375,0,0,0,0,ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
ds4server-issue-78,resident,3,16,128,69.727346,22.663208,45.361642,22.052833,22.459084,0,0,0,0,ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
ds4server-issue-78,ssd,1,16,128,11.279689,48.890291,24.849723,37.847292,48.853791,4096,32958,4194,29684662272,2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
ds4server-issue-78,ssd,2,16,128,11.520460,48.567417,25.076916,37.696334,48.779625,4096,32958,4194,29684662272,2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
ds4server-issue-78,ssd,3,16,128,11.494703,48.844875,25.066058,37.616542,48.911333,4096,32958,4194,29684662272,2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
1 implementation mode run prompt_tokens decode_tokens prefill_tps first_token_ms steady_tps p50_ms p95_ms cache_entries cache_hits cache_misses pread_bytes token_sha256
2 ds4server-baseline-79468c65 resident 1 16 128 69.627264 26.270667 39.534586 24.875959 26.607167 0 0 0 0
3 ds4server-baseline-79468c65 resident 2 16 128 69.799133 26.365458 39.505867 24.905750 26.624667 0 0 0 0
4 ds4server-baseline-79468c65 resident 3 16 128 69.618099 26.454750 39.526849 24.877584 26.585875 0 0 0 0
5 ds4server-baseline-79468c65 ssd 1 16 128 6.824429 122.398208 8.638560 114.928000 124.375958 4096 34040 7208 51017416704
6 ds4server-baseline-79468c65 ssd 2 16 128 6.805883 126.033250 8.634659 114.667833 124.220583 4096 34040 7208 51017416704
7 ds4server-baseline-79468c65 ssd 3 16 128 6.726701 125.304417 8.532966 116.431375 124.278709 4096 34040 7208 51017416704
8 ds4-8db89fe0 resident 1 16 128 56.49 22.706 45.67 21.905 22.297 0 0 0 0 ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
9 ds4-8db89fe0 resident 2 16 128 56.49 22.719 45.59 21.908 22.320 0 0 0 0 ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
10 ds4-8db89fe0 resident 3 16 128 56.51 22.638 45.60 21.890 22.342 0 0 0 0 ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
11 ds4-8db89fe0 ssd 1 16 128 12.07 46.746 25.73 36.461 46.861 4096 32958 4194 29684662272 2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
12 ds4-8db89fe0 ssd 2 16 128 11.88 46.737 26.11 35.622 46.982 4096 32958 4194 29684662272 2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
13 ds4-8db89fe0 ssd 3 16 128 11.16 47.092 26.20 35.513 46.554 4096 32958 4194 29684662272 2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
14 ds4server-issue-78 resident 1 16 128 69.930426 22.547583 45.683038 21.889833 22.275042 0 0 0 0 ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
15 ds4server-issue-78 resident 2 16 128 69.760230 22.657125 45.602401 21.936042 22.331375 0 0 0 0 ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
16 ds4server-issue-78 resident 3 16 128 69.727346 22.663208 45.361642 22.052833 22.459084 0 0 0 0 ed5461476a82824c3e0440e943a0b2f6b8a8f19dc26a550a1bc255003851ab8d
17 ds4server-issue-78 ssd 1 16 128 11.279689 48.890291 24.849723 37.847292 48.853791 4096 32958 4194 29684662272 2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
18 ds4server-issue-78 ssd 2 16 128 11.520460 48.567417 25.076916 37.696334 48.779625 4096 32958 4194 29684662272 2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
19 ds4server-issue-78 ssd 3 16 128 11.494703 48.844875 25.066058 37.616542 48.911333 4096 32958 4194 29684662272 2bc33a04bb88f9f3c56f8bae0124b7bf25fd2f32aee81a1bdaf6e979448da052
@@ -0,0 +1,100 @@
# Qwen: MTPLX versus DS4Server GPU responsiveness
Measured on the same M5 Max on 2026-09-05. This is a responsiveness diagnosis,
not acceptance of the goal to match reference throughput within 2%.
## Method
- Installed Qwen3.8 Flash Next Bare Speed artifacts only; identical model path
for both implementations. No downloads, offline Hugging Face mode for MTPLX.
- Context capacity 131072, prefill chunks 2048, reasoning low, temperature 1,
top-p 0.95, top-k 20, seed 12345. DS4Server power 100%; MTPLX has no imposed
throttling, fan mode default, turbo profile.
- The exact raw system prompt and user prompts were exported from DS4Server.
Warmup is separate; README summary, story and Python generation form one chat.
Each response runs to EOS, not a fixed-token performance sample.
- Same native `ds4_gpu_canary_probe`: a 4096-byte Metal blit on an independent
queue, in the model process, nominally every 100 ms. Native commit-to-scheduled
and commit-to-completed times exclude Python scheduling delay. The Python
wrapper separately records host scheduling lag.
- MTPLX CLI `exec` handoff is intercepted only to invoke its ordinary server
entry point in the instrumented process; inference code is unchanged.
- Separate supervisor: 120 seconds without startup/inference progress or a
physical footprint above 116823110451 bytes terminates the child. Canary
activity alone does not renew inference progress. No overall runtime timeout.
- Runs are sequential. Both valid MTPLX AR runs and the MTP run completed all
three responses normally. All probes reported successful completion.
Reference: MTPLX commit `e652d55e2652137a4abcf1312357abbf3eb9d692`, MLX 0.32.2.
DS4Server: dirty working tree based on `b99ce2a`; no scheduling fix applied for
these measurements. The exported JSON records the complete effective settings.
## Results
Maximum native GPU-canary completion latency in milliseconds:
| Run | Prefill | Decode | Warmup |
| --- | ---: | ---: | ---: |
| DS4Server AR, exported-input control | 4090.027 | 17.903 | see raw JSON |
| MTPLX AR, first valid run | 7.610 | 0.527 | 0.894 |
| MTPLX AR, explicit session repeat | 2.974 | 0.713 | 2.854 |
| MTPLX MTP, explicit session | 0.550 | 0.696 | 1.460 |
Thus the multi-second GPU queue delay is reproduced in DS4Server but was **not
observed in MTPLX**, including a complete MTP conversation. This does not prove
that MTPLX can never stall or replace measurement of an actual GUI event loop.
## Source-level differences and comparison caveats
MLX does not put an entire lazy evaluation into one command buffer. Its
[`CommandEncoder::needs_commit`](https://github.com/ml-explore/mlx/blob/v0.32.2/mlx/backend/metal/device.cpp)
checks operations and referenced buffer bytes; the Max defaults are 50 operations
and 50 MiB, with environment overrides. The
[`eval` path](https://github.com/ml-explore/mlx/blob/v0.32.2/mlx/backend/metal/eval.cpp)
commits asynchronously when those thresholds are exceeded. These are batching
thresholds, not a guarantee of a maximum GPU duration.
DS4Server Qwen `eval_prefill_batch` currently finishes only after all layers at
power 100 unless layer profiling is enabled. This differs from MLX's automatic
submission boundaries. No smaller prefill chunk is needed to investigate this
specific scheduling difference.
The raw inputs are identical, but rendered tokens are not yet identical:
DS4Server summary has 7490 prompt tokens versus MTPLX's 7460. The Qwen
`encode_continuation` path calls `encode_qwen_messages` with an empty system
string, which still emits the low-reasoning system instruction. That inserts
another system message when extending a checkpoint. Full-conversation rendering
does not repeat that instruction for every user turn. This must be fixed and
tested independently before claiming exact input parity.
Update after these measurements: the continuation renderer has been corrected
locally and the focused token-only regression test passes. These historical
measurements still describe the pre-fix binary; they are not post-fix evidence.
The complete execution-chain inventory and follow-up work are tracked in the
[reference-parity audit](/Users/gb/Projects/DS4Server/docs/inference-reference-parity-audit-20260905.md).
MTPLX AR reused 5120 cached tokens on every turn, even with an explicit session
header. MTP reused 5120, 7840 and 8861. Both sent complete ongoing history, but
their cache reuse is different. Native MTPLX `timings.prompt_per_second` divides
the whole prompt by prefill time; for fair suffix throughput use
`(prompt_tokens - cached_tokens) / prefill_seconds`. Neither this number nor raw
total chat time is currently a matched-work 2% acceptance result.
## Local evidence
All artifacts are in `/private/tmp/ds4-ongoing-verified.16vzVw/`:
- `qwen-export.jsonl`: DS4Server inputs, all outputs, per-turn metrics, canary.
- `mtplx-ar-v4.jsonl`: first successful MTPLX AR conversation.
- `mtplx-ar-session.jsonl`: repeated AR with explicit session ID.
- `mtplx-mtp.jsonl`: MTP with explicit session ID.
- `mtplx_probe.py`: supervised reference runner; invoke with `ar` or `mtp`
using the reference checkout's `.venv/bin/python`. Each starts and stops its
own server on localhost port 18091.
- `libds4_probe.dylib`: unchanged DS4Server Metal bridge object, linked with
dead stripping and only `ds4_gpu_canary_probe` exported. No DS4 C inference
implementation is linked or invoked.
Earlier `mtplx-ar.jsonl`, `mtplx-ar-v2.jsonl`, and `mtplx-ar-v3.jsonl` are failed
instrumentation attempts (linking, CLI exec losing threads, Python module
identity respectively), **not benchmark evidence**.
+89
View File
@@ -0,0 +1,89 @@
# Qwen vision verification — 2026-09-11
Qwen3.8 Flash Next now accepts the existing PNG/JPEG attachments. Its optional
vision encoder has a separate Model Manager entry; downloading, validating or
deleting it does not alter the text-model artifact set. The existing keep-vision-
weights-loaded preference also applies to Qwen. Application preprocessing,
loading and inference are Rust, using the existing Metal runtime kernels.
The four pinned vision files (897,900,287 bytes total) were downloaded and SHA-256
verified in `~/Library/Application Support/de.rfc1437.ds4server/models/qwen3.8-flash-next`.
The artifact revision is `74559cdf34fbfc0b593de72d17e93f37fd4f9ea7` of
`Youssofal/Qwen3.8-Flash-Next-MTPLX-Bare-Speed`; the text manifest remains unchanged.
The video processor configuration is part of that artifact set; this change
implements still images.
## Grounding check
Every primary description run used exactly `Describe this image`. The supplied
1024×1024 PNG was copied unchanged to
`local-eval-results/qwen-vision-input/image.png`. Its SHA-256 is
`b7568d90f4df6180d9af14a824dd553cd995457561d29167354c1ba66b728347`.
Only the image bytes and prompt enter the model; neither the source filename nor
the neutral filename is included in its text input.
Rust/Metal generation succeeded both with MTP and with ordinary autoregressive
decoding. Qwen described a golden-tan cartoon llama/alpaca, large eyes, upright
ears, an open smiling mouth, mountains, a sunset and a grainy poster texture.
These details are visible in the supplied image. The direct cold-session result
begins:
> This is a stylized, cartoon-style illustration of a llama (or alpaca) shown from the neck up, set against a sunset landscape.
Controls used the same prompt:
| Input / session | Observed result |
| --- | --- |
| No image, Rust and MTPLX | Reports no attached image and requests one |
| Solid blue image, same dimensions, after the animal image | Describes a uniform blue field; zero cached prompt tokens |
| Same image repeated | Same description; all 1,069 prompt tokens reused |
| Saved checkpoint, reset, restore, follow-up | Correct animal description; 1,446 cached tokens out of 1,460 |
This demonstrates image-dependent descriptions for these inputs, not a general
guarantee against hallucinations.
## Oracle and reproducibility
The oracle is local MTPLX reference `e652d55` with MLX 0.32.2. Python scripts under
`tools/qwen-vision*-reference.py` run only that reference, never the application.
The Rust tower matches its exported values exactly at patch embedding, position
embedding, rotary positions, blocks 0 and 26, and the final merger. Both the
1024×1024 input (2,621,440 final values) and a small non-square fixture (168,960
final values) had zero differing values. CPU resize/preprocessing also matches
three Pillow/MTPLX golden hashes. This is exact encoder agreement; full generated
token-sequence parity is not claimed.
Local evidence is retained under `local-eval-results/`:
- `qwen-vision-rust-mtp.jsonl`, `qwen-vision-rust-ar.jsonl`,
`qwen-vision-rust-no-image.jsonl`: complete application runs.
- `qwen-vision-lifecycle.jsonl`: cold, repeat, restored and changed-image runs.
- `qwen-vision-chat-reference.jsonl`: oracle image/no-image runs.
- `qwen-vision-image/`, `qwen-vision-small/`: exported oracle arrays.
- `qwen-vision-small-rust.log`: small-fixture exact comparison.
Example application invocation (empty YAML config avoids an unrelated system
prompt):
```sh
target/release/ds4-server model-eval \
--model qwen3.8-flash-next --config /tmp/qwen-vision-config.yaml \
--prompt 'Describe this image' \
--image-file local-eval-results/qwen-vision-input/image.png \
--context 8192 --max-tokens 1024 --reasoning low \
--temperature 0 --top-p 0.95 --seed 1 --acceleration on \
--prefill-chunk 2048 --warmup off --canary on --max-memory-gib 108
```
The ignored GPU tests `qwen_vision_tower_matches_mtplx_image` and
`qwen_vision_chat_checkpoint_preserves_image_identity` are runnable with
`DS4_QWEN38_ARTIFACTS` pointing to the model directory and respectively
`DS4_QWEN_VISION_REFERENCE` pointing to exported arrays or
`DS4_QWEN_VISION_IMAGE` pointing to the neutral input. Run one GPU model process
at a time under `test-supervisor` with an appropriate memory limit.
Final checks: `cargo fmt --all -- --check`, Clippy with all targets/features and
warnings denied, `RUST_TEST_THREADS=1 cargo test --all-features` (315 passed,
204 opt-in tests ignored), and `make bundle` all succeeded. The two encoder
comparisons and image lifecycle test were additionally executed explicitly with
GPU access. The updated, signed application is `target/release/DS4Server.app`.
+18
View File
@@ -0,0 +1,18 @@
date,engine,mode,sequence,run,state,context,prompt_tokens,generated_tokens,temperature,top_p,top_k,seed,prefill_tps,first_token_ms,steady_tps,decode_tps,p50_cycle_ms,p95_cycle_ms,peak_rss_bytes,token_sha256,cycles,drafted,accepted,verifier_passes,verifier_ms,repair_ms,ple_entries,ple_hits,ple_misses,ple_evictions,ple_pread_bytes,ple_pread_ms,notes
2026-09-04,DS4Server,MTP,B1,1,cold,4096,26,64,1.0,0.95,20,12345,6.360708,59.484667,57.518099,58.042801,54.731042,57.838709,52720156672,46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72,20,60,44,20,1000,0,1648,432,1648,0,164800,,repair timing counter not yet wired
2026-09-04,DS4Server,MTP,B1,2,warm,4096,26,64,1.0,0.95,20,12345,51.095361,52.229584,60.307983,61.119864,52.427709,54.273459,52720156672,46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72,20,60,44,20,946,0,1648,2080,0,0,0,0,repair timing counter not yet wired
2026-09-04,DS4Server,MTP,B1,3,warm,4096,26,64,1.0,0.95,20,12345,51.152493,52.422375,60.014218,60.825779,52.679334,54.147250,52720156672,46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72,20,60,44,20,951,0,1648,2080,0,0,0,0,acceptance median 60.825779
2026-09-04,DS4Server,MTP,B2,1,cold,4096,26,64,1.0,0.95,20,12345,6.527240,62.605750,54.320764,54.834156,57.909625,60.828916,52605190144,46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72,20,60,44,20,1056,30,1648,432,1648,0,164800,63,confirmation after repair telemetry
2026-09-04,DS4Server,MTP,B2,2,warm,4096,26,64,1.0,0.95,20,12345,50.178762,54.061500,57.196397,58.019436,55.002750,57.242875,52605190144,46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72,20,60,44,20,993,29,1648,2080,0,0,0,0,confirmation median 58.019436
2026-09-04,DS4Server,MTP,B2,3,warm,4096,26,64,1.0,0.95,20,12345,49.976289,55.289000,57.556598,58.301539,54.853000,56.619333,52605190144,46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72,20,60,44,20,988,30,1648,2080,0,0,0,0,confirmation after repair telemetry
2026-09-04,DS4Server,AR,A1,1,cold,4096,26,64,1.0,0.95,20,12345,7.796453,21.087125,49.845055,49.805297,18.894042,19.426917,53962735616,15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d,0,0,0,0,0,0,1648,16,1424,0,,,plain autoregressive
2026-09-04,DS4Server,AR,A1,2,warm,4096,26,64,1.0,0.95,20,12345,51.386057,18.606375,51.034286,51.074537,18.392375,18.953542,53962735616,15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d,0,0,0,0,0,0,1648,,,,,,plain autoregressive
2026-09-04,DS4Server,AR,A1,3,warm,4096,26,64,1.0,0.95,20,12345,51.424682,18.256166,50.610874,50.671077,18.575958,19.129667,53962735616,15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d,0,0,0,0,0,0,1648,,,,,,plain autoregressive median 50.671077
2026-09-04,DS4Server,AR,LONG,1,warm,2048,,1,,,,,0.778000,,,29.259000,,,52158349312,b6d28407,,,,,,,,,,,,,synthetic long-context state gate token=5 live_state_bytes=58195968
2026-09-04,DS4Server,AR,LONG,1,warm,16384,,1,,,,,12.101000,,,46.486000,,,,79d71772,,,,,,,,,,,,,synthetic long-context state gate token=5 live_state_bytes=465567744
2026-09-04,DS4Server,AR,LONG,1,warm,65536,,1,,,,,10.037000,,,45.739000,,,,0a1131fe,,,,,,,,,,,,,synthetic long-context state gate token=5 live_state_bytes=1862270976
2026-09-04,DS4Server,AR,LONG,1,warm,131072,,1,,,,,3.277000,,,45.145000,,,,fb678d10,,,,,,,,,,,,,synthetic long-context state gate token=5 live_state_bytes=3724541952
2026-09-04,DS4Server,AR,LONG,1,warm,262144,,1,,,,,0.777000,,,43.488000,,,,01ae755c,,,,,,,,,,,,,synthetic native-maximum state gate token=5 live_state_bytes=7449083904
2026-09-04,MTPLX,MTP,ORACLE,1,warm,4096,26,64,1.0,0.95,20,12345,,,,59.170000,,,,,20,58,44,20,950,,,,,,,,fresh matched run before local source-package drift
2026-09-04,MTPLX,MTP,ORACLE,2,warm,4096,26,64,1.0,0.95,20,12345,,,,59.820000,,,,,20,58,44,20,950,,,,,,,,fresh matched run before local source-package drift
2026-09-04,MTPLX,MTP,ORACLE,3,warm,4096,26,64,1.0,0.95,20,12345,,,,59.751170,,,,,20,58,44,20,950,,,,,,,,matched median 59.751170
1 date engine mode sequence run state context prompt_tokens generated_tokens temperature top_p top_k seed prefill_tps first_token_ms steady_tps decode_tps p50_cycle_ms p95_cycle_ms peak_rss_bytes token_sha256 cycles drafted accepted verifier_passes verifier_ms repair_ms ple_entries ple_hits ple_misses ple_evictions ple_pread_bytes ple_pread_ms notes
2 2026-09-04 DS4Server MTP B1 1 cold 4096 26 64 1.0 0.95 20 12345 6.360708 59.484667 57.518099 58.042801 54.731042 57.838709 52720156672 46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72 20 60 44 20 1000 0 1648 432 1648 0 164800 repair timing counter not yet wired
3 2026-09-04 DS4Server MTP B1 2 warm 4096 26 64 1.0 0.95 20 12345 51.095361 52.229584 60.307983 61.119864 52.427709 54.273459 52720156672 46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72 20 60 44 20 946 0 1648 2080 0 0 0 0 repair timing counter not yet wired
4 2026-09-04 DS4Server MTP B1 3 warm 4096 26 64 1.0 0.95 20 12345 51.152493 52.422375 60.014218 60.825779 52.679334 54.147250 52720156672 46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72 20 60 44 20 951 0 1648 2080 0 0 0 0 acceptance median 60.825779
5 2026-09-04 DS4Server MTP B2 1 cold 4096 26 64 1.0 0.95 20 12345 6.527240 62.605750 54.320764 54.834156 57.909625 60.828916 52605190144 46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72 20 60 44 20 1056 30 1648 432 1648 0 164800 63 confirmation after repair telemetry
6 2026-09-04 DS4Server MTP B2 2 warm 4096 26 64 1.0 0.95 20 12345 50.178762 54.061500 57.196397 58.019436 55.002750 57.242875 52605190144 46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72 20 60 44 20 993 29 1648 2080 0 0 0 0 confirmation median 58.019436
7 2026-09-04 DS4Server MTP B2 3 warm 4096 26 64 1.0 0.95 20 12345 49.976289 55.289000 57.556598 58.301539 54.853000 56.619333 52605190144 46ce63601c44d80b5ef8297eaa49a636f6680040b916fe30cf7a154c05daef72 20 60 44 20 988 30 1648 2080 0 0 0 0 confirmation after repair telemetry
8 2026-09-04 DS4Server AR A1 1 cold 4096 26 64 1.0 0.95 20 12345 7.796453 21.087125 49.845055 49.805297 18.894042 19.426917 53962735616 15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d 0 0 0 0 0 0 1648 16 1424 0 plain autoregressive
9 2026-09-04 DS4Server AR A1 2 warm 4096 26 64 1.0 0.95 20 12345 51.386057 18.606375 51.034286 51.074537 18.392375 18.953542 53962735616 15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d 0 0 0 0 0 0 1648 plain autoregressive
10 2026-09-04 DS4Server AR A1 3 warm 4096 26 64 1.0 0.95 20 12345 51.424682 18.256166 50.610874 50.671077 18.575958 19.129667 53962735616 15373689bcb1c667123accfcfbe49736466f28ce7beb89d2cbe5e4120416056d 0 0 0 0 0 0 1648 plain autoregressive median 50.671077
11 2026-09-04 DS4Server AR LONG 1 warm 2048 1 0.778000 29.259000 52158349312 b6d28407 synthetic long-context state gate token=5 live_state_bytes=58195968
12 2026-09-04 DS4Server AR LONG 1 warm 16384 1 12.101000 46.486000 79d71772 synthetic long-context state gate token=5 live_state_bytes=465567744
13 2026-09-04 DS4Server AR LONG 1 warm 65536 1 10.037000 45.739000 0a1131fe synthetic long-context state gate token=5 live_state_bytes=1862270976
14 2026-09-04 DS4Server AR LONG 1 warm 131072 1 3.277000 45.145000 fb678d10 synthetic long-context state gate token=5 live_state_bytes=3724541952
15 2026-09-04 DS4Server AR LONG 1 warm 262144 1 0.777000 43.488000 01ae755c synthetic native-maximum state gate token=5 live_state_bytes=7449083904
16 2026-09-04 MTPLX MTP ORACLE 1 warm 4096 26 64 1.0 0.95 20 12345 59.170000 20 58 44 20 950 fresh matched run before local source-package drift
17 2026-09-04 MTPLX MTP ORACLE 2 warm 4096 26 64 1.0 0.95 20 12345 59.820000 20 58 44 20 950 fresh matched run before local source-package drift
18 2026-09-04 MTPLX MTP ORACLE 3 warm 4096 26 64 1.0 0.95 20 12345 59.751170 20 58 44 20 950 matched median 59.751170
@@ -1,7 +1,6 @@
MIT License MIT License
Copyright (c) 2026 The ds4.c authors Copyright © 2023 Apple Inc.
Copyright (c) 2023-2026 The ggml authors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+41
View File
@@ -0,0 +1,41 @@
MTPLX
Copyright 2026 Youssof Altoukhi
MTPLX is a native MTP speculative decoding project for Apple Silicon.
ATTRIBUTION REQUIREMENT
This NOTICE file is part of the Apache License 2.0 terms for MTPLX (see
section 4(d) of the LICENSE). Any product, application, service, or
distribution that includes, embeds, or is built on MTPLX, in whole or in
part, modified or unmodified, must display the following attribution within
the product itself, in a place a user of that product can see (for example an
About screen, a credits or acknowledgements screen, a settings or help page,
documentation shipped with the product, or the startup banner of a command
line tool):
Powered by MTPLX
https://github.com/youssofal/mtplx
Attribution in a source repository, a README, or a marketing page alone does
not satisfy this requirement. The words "Powered by MTPLX" must appear
in-product. The link is required wherever the display medium supports it.
Public benchmarks, articles, and research that use or build on MTPLX should
credit "MTPLX by Youssof Altoukhi" with the same link.
If MTPLX informs academic or technical writing, please cite the repository using
the included CITATION.cff metadata.
This distribution includes a vendored standalone subset of vllm-metal's
Apache-2.0 licensed Metal paged-attention kernels under vllm_metal/metal.
The vendored subset is used only for local MLX/Metal kernel dispatch and does
not include or depend on the vLLM serving stack.
This product includes Metal kernel code adapted from dflash-mlx
(https://github.com/bstnxbt/dflash-mlx), Copyright dflash-mlx contributors,
licensed under the Apache License 2.0. See mtplx/nax_verify.py for details.
This product includes the Apache-2.0 licensed MLX implementation for
Laguna-S-2.1 from PipeNetwork, Copyright 2026 PipeNetwork, under
mtplx/models/laguna.py.
+551
View File
@@ -0,0 +1,551 @@
MTPLX Qwen runtime shaders
=========================
mtplx-runtime-0.32.2.metallib is an unchanged copy of mlx/lib/mlx.metallib
from the existing MTPLX reference environment (runtime version 0.32.2).
SHA256: dc59d1cceb1a5c7e578232e6e41e28e2c73c9463ac6dbc3886c3ee17ffc270ed
Source tag: v0.32.2
Source commit: 1f8e74e3f12f31365464a6867c6579f0e9b29d85
Source: https://github.com/ml-explore/mlx/tree/v0.32.2
License: MIT, reproduced in MLX-LM-LICENSE.txt (identical license text).
Only GPU shader code is included. No libmlx.dylib, Python inference code,
or C/C++ application/backend code is linked into DS4Server. Rust selects
the shader entry points, binds model buffers and owns graph execution.
The reference's get_quantized_kernel implementation was verified to call
Device::get_kernel on the default precompiled library, matching
mlx/backend/metal/nojit_kernels.cpp at the pinned commit. The shader library
is reused byte-for-byte, including its precompiled specializations, rather
than independently rebuilding different kernels or compiler settings.
Provenance on the evaluation machine:
/private/tmp/MTPLX-analysis-20260901/.venv/lib/python3.12/site-packages/mlx/lib/mlx.metallib
The Rust test mtplx_runtime_shaders_are_pinned enforces the complete file
hash. Existing resource packaging includes the metal directory recursively.
This artifact adds approximately 174 MiB to the resources; it is not a model
download. Loading is lazy on the first runtime-shader dispatch.
The shader identity is not a claim that the complete Rust model graph,
scheduling, cache behavior, or end-to-end performance has reached parity.
Compiled QSA moving offsets
--------------------------
tests/fixtures/mtplx-qsa-update-jit.json records the actual compiled indexer
scalar shaders and compute_dynamic_offset_int32, observed without changing
the original compilation by tools/mtplx-jit-reference.py --operation qsa-update.
Only the contiguous int32[1] scalar specializations are retained; full observed
library source hashes are included. --check reruns the original compiled core.
The Dynamic Offset body is used unchanged by the Rust dynamic-copy path;
its original full source hash is
48a7309664f797e749aa42d2c2c4db0cf3abedf97297f0068d06b7847d988b93.
It is Copyright Apple Inc., MIT as reproduced in MLX-LM-LICENSE.txt.
The following gg1/gg2_dynamic_copybfloat16bfloat16 kernels are taken directly
from the unchanged runtime metallib. No frontiers are read back to the CPU.
Clamp/Multiply now feed the connected qsa_compiled_cache_window stage.
mtplx-qsa-compiled-scalars.json retains the actual generated scalar kernels,
including all three constant-CSE layouts for the Clamp and an independent
257/255 specialization check. Only structural integer literals and exported
symbols change when Rust specializes a kernel; the computations are unchanged.
mtplx-qsa-compiled-header.metal is the unmodified original compiler prefix;
SHA256: 2665a76463f3f6ee283c6a50b66e4a527318a114080b31441dfa900042097a39.
--qsa-header --check compares that prefix with a fresh original compilation.
Both resources contain runtime shader code, not host runtime code. Most is
Apple MIT. The unchanged full prefix also retains the Apache-2.0 cexpf.h
notice (Apple, NVIDIA, Filipe RNC Maia; license text in MTPLX-LICENSE.txt) and
the full BSD-2-Clause expm1f.h notice/disclaimer (Norbert Juffa 2015-2023).
Those overloads are retained as original header dependencies, not new Qwen
complex/exponential computation in the integer scalar kernels.
26 compiled reference calls verify the complete retained-input cache window.
They do not establish graph-bank replay, allocation/donation, BFS scheduling
or production inference parity.
tests/fixtures/mtplx-qsa-select-jit.json also retains the actual compiled Add
kernels for selector row offsets 0/1/2/3. Rust substitutes only the structural
offset literal and exported symbol, preserving the original +0 dispatch.
--operation qsa-update --qsa-mode blocks --qsa-score-budget 4096 --qsa-header
--check reproduces the connected reference selector and its exact sources.
Query preparation and both selector families now accept GPU frontier leaves
through the same original kernel dispatch used by the host-frontier entry.
66 actual compiled reference calls cover all five output modes, chunked
selection and the connected cache state for retained old input leaves. These
are functional Q/K-entry checks, not graph-bank, ownership or UI performance
acceptance. Host integration and the runtime evaluator remain open.
The retained-input Hidden entry has since been connected through the already
verified original affine projection kernels to that same Q/K implementation.
132 actual select_hidden calls cover 4/8-bit, group32/64 projections and all
five output modes. The combined entry test covers 198 reference calls. No new
Metal bodies or alternative projection/selection arithmetic are introduced.
The installed B1/BF16 cache/phase routing is now connected to backing reserve,
explicit GPU frontiers, those same retained-input arithmetic entries and cache
commit. 720 actual original host-method decisions and 108 additional complete
indexer calls cover routing and ongoing state/output transitions. The combined
host-flow test includes the previous 92 non-compiled calls through the same
entry. A parameter-bound QSA graph bank now replaces the direct compiled
expression chains. It rebinds explicit inputs to cached primitive dependencies
and uses the pinned degree/BFS-width algorithm for a single indexer graph.
99 optimized original graph contracts, all 198 arithmetic calls, stride-changing
replay, parameter invalidation and the original connected-call trace/entry
counters pass. Evaluated constants are omitted from structural fingerprints;
kernel-source and output checks remain separate. No alternate Metal kernel was
introduced. The dtype/shape-generic guard, donation/allocator, early release,
global model scheduling and production integration remain open. Last-use
graph leaves are now detached after their consumer, separately from explicit
completion ownership that protects GPU work until its existing CB finishes.
The shared canonical dispatch bridge holds bound Metal resources through
completion as well; this fixes four Invalid Resource failures exposed with
unretained command buffers. Compile/dispatch use scoped autorelease pools.
The normal and strengthened model-free collections cover 34 tests. This is
not performance acceptance or evidence of matching whole-model Metal encoder
timelines. Donation must still account for both descriptor and shared Data
ownership, including outstanding GPU evaluator holds.
The canonical test-bound Buffer now separates array/view identity from shared
Data ownership, including nested native views and completion holds. QSA COW
checks both Rc<Buffer> sharing and underlying Data sharing. A direct pinned
QSACache alias/view update and its negative Rust regression check prove that
array aliases observe replacement while distinct views retain old values.
The QSA graph now applies the pinned primitive input/sibling-minus-primary-output
Data retention protocol before the next primitive, rather than re-holding leaves
at their last consumer. Its scheduler tracks the actually selected sibling, not
just the producer node. CPU scheduling and GPU ownership-count checks cover that
distinction, duplicate Data, empty-batch fallback and completion.
QSA raw/pool DynamicSliceUpdate now performs actual BF16 vector-copy donation
for exclusive mutable cache state. Retained inputs, snapshots, views and GPU
Data holds select copying instead. The 16 KiB bound uses root allocation size.
All 198 core reference cases also run with state snapshots and exclusive state,
checking old/new hashes and actual Data reuse. No kernels or synchronization
boundaries changed. Model-wide integration, other primitive donation and generic
dtype/layout contracts remain open.
The allocator policy is now ported from the pinned buffer_cache.h with the same
best-fit/oldest-equal-size choice, strict reuse ceiling and age-based/90%-clear
eviction. A 101-event trace from the real installed runtime checks allocation
identities and active/cached bytes, including cache-limit transitions. Reference page rounding and the
device maxBufferLength precheck are connected to the test-bound Buffer methods;
logical views preserve tensor bounds while Data records the rounded root size.
The explicit Rust Allocator now owns a real 1 MiB untracked/shared Metal heap,
uses it for requests below 256 bytes with device-allocation fallback, and
recycles native roots only after the final physical allocation hold releases.
Its active/cache/peak/resource accounting, cache and memory limits, resource
pressure GC (including the original unsigned subtraction), zero/null result
and actual cached storage reuse are checked against the 101-event receipt.
Residency/wired limits are now connected to this explicit allocator, including
heap registration, cache retention and erase-before-release. Set selection and
budgets follow resident.cpp: first fit, oversize/empty-set reuse, 32-set ceiling,
emptiest fallback and touched-set commits on resize. Native membership and the
ten original residency lifecycle scenarios are tested. Queue attachment uses
the published set count and is exercised immediately before test commits;
The new test-bound Submission owner now attaches automatically at actual native
commit boundaries through a scoped encoding-thread callback, including flush,
readback, finish and cleanup paths. Its queue cursor persists across batches;
scope teardown drains before unregistering the callback. Legacy work outside
the scope and other queues do not inherit it. Externally wrapped storage,
the process-wide owner and model-wide routing remain open.
Existing canonical helpers
are not globally switched to untracked buffers before encoder dependencies
are ported. No whole-model allocator/performance parity is claimed.
The test-bound Rust Encoder now owns an independent queue with unretained
command buffers and Concurrent compute encoders, following the pinned
device.cpp/event.cpp/error.h dependency and completion rules. Access roles,
barrier epochs, deferred concurrent outputs, cross-encoder fences, temporary
exclusion and shared-event error propagation are managed in Rust. The bridge
only issues Metal API calls and reuses the same original kernel dispatcher.
Commit thresholds count array.data_size() ELEMENTS (as the reference does),
not allocation bytes; counters persist across encoder boundaries. The three
new checks include dependent untracked GPU copies, two-queue event transfers
and safe synthetic error-completion tests. The 48-test suite passes in both
legacy retention modes. Only these new encoder tests use the independent
queue. Complete operator access metadata, stream/evaluator integration and
production routing are still open; this is not a model-performance receipt.
Operator scopes now connect the existing normalization and full MoE chain to
the independent Concurrent encoder and its pooled untracked allocator. Explicit
binding roles/data_size spans cover routing, sorting, gather/unsort, Gate/Up,
SwiGLU, casts/norms, affine and gathered quantized projections, Split-K and both
stock/fused experts plus the shared expert. Sort and split-reduction scratch is
registered as backend temporaries. RMSNorm's default one and GatherSort's divisor
are real scalar array bindings, not setBytes replacements. No shaders changed.
The nine existing fixture groups (540 cases) execute through BOTH encoders with
unchanged reference-output checks. This does not multiply independent fixtures.
QSA routing, general array/donation semantics, evaluator/stream integration,
production instrumentation and complete model/performance acceptance remain open.
GDN routing now uses the same typed dispatch/allocator path, including conv,
mask/cache, Q/K normalization, compute_g/beta, recurrence, fused step and output.
The direct native fused-step bypass is removed. Scalar operands and custom T
are original scalar array inputs. Concatenate uses the original concurrent
disjoint slice writes and dependency join; checked-input copies retain the
original order and backend-temporary registration. No shader bodies changed.
Four further existing groups (476 GDN cases) execute in both encoders, bringing
the dual-encoder total to 1,016 existing cases. This remains operator-level
correctness coverage, not whole-model scheduling or production parity.
QSA static/dynamic copies now bind explicit array data_size metadata rather
than treating the copied region as the whole bound array. Dynamic offset arrays
are inputs/backend temporaries. Zero fill, COW/General copies, compiled frontier
operations and fused query/pool preparation carry access roles. KV concatenate
uses the reference concurrent slice-write region. The existing 12 backing/copy
and 26 compiled cache-window cases now run in both encoders (1,054 existing
dual-encoder cases in total). Eager preparation, score/select operators and full
graph/production integration remain open. Shader sources/geometries unchanged.
Eager QSA preparation now carries access roles and exact slice spans through
RoPE, mean/RMS pooling and projections; all three RoPE concatenates use the
reference concurrent writer regions. Scalar arithmetic uses array inputs.
The fused selector and chunk concatenation also use the same encoder; its three
frontiers are distinct scalar arrays and its private score plane remains a
custom-kernel output, not a backend temporary. The existing 210 preparation,
88 ongoing cache and 75 selector/chunk cases now execute in both encoders,
bringing the existing dual-encoder total to 1,427 cases. Empty-array placeholder
storage is unchanged; general zero-storage array semantics and the remaining
score/eager-selection, evaluator and production paths are still open.
Both QSA prefill score producers and the connected score/top-k/chunk route now
use the independent encoder. FP32 GEMV/Steel/NAX/Split-K, ReLU and reduction
carry explicit array spans; Maximum/Divide scalars are array inputs. Matmul's
output is allocated before transpose-copy preparation; those copies and the
Split-K plane are registered as backend temporaries after their consumers.
The MPP producer retains its original stride-aware inputs. Prefill top-k
allocates outputs before checked-input copies, registers those copies after
dispatch and uses three separately allocated frontier arrays. Shader bodies,
specializations and dispatch geometries are unchanged. The existing 384 FP32
score, 146 MPP/top-k/prefill and 96 mixed-producer cases now run in both encoders,
bringing dual-encoder coverage to 2,053 existing cases. These manual fixture
batches do not establish general evaluator/stream boundaries or production
parity; eager selection/output and full graph integration remain open.
Eager QSA score masking, ranking and all output branches now carry explicit
input/output array spans through the independent encoder. Scalar operands are
real arrays; per-tile mx.eval(top_t) synchronizes that queue before constructing
the next tile. Tiled index concatenation, decode-tail concatenation, rows-gather
outputs and dense padding use the original concurrent slice-write regions.
Dense padding is materialized before concatenation, and rows-gather builds its
two separate Arange expressions; these restore previously collapsed operations.
No Metal body or specialization changed.
The fixed-signature indexer graph now defers input/sibling Data on the active
queue rather than retaining fallback descriptors in its returned state. The
selected output's Data remains excluded, preserving donation. Compiled chunk
concatenation joins concurrent writers and moving frontiers are four separate
int32 arrays. Existing empty/nonempty Data-ownership checks run in both queues.
The existing 540 eager cases and 200 ongoing indexer calls (including compiled
routes) also execute in both encoders: 2,793 existing dual-encoder cases total.
Explicit tile eval counts, output/state hashes and graph engagement are checked.
These bounded fixture batches still do not apply the complete model-wide
evaluator/stream scheduler and its automatic primitive-level commit policy.
Production routing/instrumentation and whole-model performance acceptance remain
open, as does general zero-storage array handling.
Primitive submission and task scheduler
--------------------------------------
The fixed indexer tape now checks the original encoder thresholds after a whole
primitive, never after an individual kernel dispatch. Input/sibling Data and
backend holds are attached before a possible commit. Counted GPU tasks complete
from the command callback; failed submissions balance ownership/accounting.
The single-stream tape also applies the original active-task/memory pressure
condition, finalizes its stream and waits for progress without inserting sleeps.
The Rust scheduler uses stdlib FIFO workers per CPU stream, earliest-error
preservation, non-consuming cross-stream event error propagation and draining
shutdown. CPU event waits/signals use the existing Metal shared-event bridge.
Explicit tile/final fixture synchronizations are checked separately from
automatic commits, with independent expectations for the two full-prefill
indexer cases that cross the pinned Max data-size threshold.
CPU dispatch now counts every tenth operation, with completion as a separate
FIFO task so failed work still completes its activity accounting. The CPU
primitive cleanup task participates in that count and retains complete backend
temporary descriptors until earlier work has run. CPU and GPU now share the
input/sibling Data-selection function, excluding donated primary output Data.
The private CPU temporary wrapper is Send only for drop-only worker ownership;
Buffer itself remains !Send/!Sync. Five scheduler tests and the existing GPU
ownership/connected-indexer checks pass, including unretained indexer execution.
An explicit runtime-owned stream registry now connects CPU/GPU encoders for the
installed single-CPU/single-Metal backend. Defaults and template resolution are
per thread/device. Local encoders are destroyed at thread exit; global streams
allow sequential cross-thread use. Explicit clear preserves the reference's
metadata/stale default handles and global CPU/GPU cleanup distinction.
Registry locks do not cover encoding or waits. GPU selection reuses the same
Rc/TLS encoder and allocator; exclusive ownership is checked before returning
an encoder to storage. CPU/GPU events and finalize-all pressure handling use
the existing scheduler and native bridge. Two new checks include actual
two-queue copies, eleven blocked CPU tasks, thread cleanup and error unwinding.
The 200 ongoing indexer cases now use a registered GPU stream in the independent
path; the full model-free collection passes 57/57 normal and unretained.
Graph events now preserve per-copy values/origin streams while sharing native
events and errors. Inter-stream fences use either the reference SharedEvent
path or its opt-in Metal3/macOS15 fast path. Fast synchronization remains off
by default. The existing pinned metallib supplies input_coherent, fence_update
and fence_wait unchanged. Rust preserves array output registration, raw timestamp
bindings, explicit update barriers, cross-device coherence and CPU SeqCst
timestamp operations. Shared fence counts and per-dispatch snapshots are
separate; completion/task ownership retains timestamp storage until work ends.
The bridge now accepts empty dispatch grids, including the original zero-work
coherence dispatch. Three new checks cover events/errors, partial-word/empty
coherence and CPU/GPU/GPU/CPU transfer in both modes with a test-only deadlock
rescue. All 60 model-free tests pass normal and unretained; no rescue fired.
No shader body, metallib, inference default or production routing was changed.
This is still test-bound. Full array/evaluator cross-stream dependency construction, actual
CPU model primitives, compile-cache cleanup integration and the complete model
graph remain open. Production Qwen and performance acceptance remain unchanged;
stream/operator checks do not establish whole-model parity.
Canonical dispatch now transfers deduplicated Rust root-allocation ownership
to its existing Metal completion callback. This replaces the native resource
set in that path and prevents physical release without adding Data aliases
that would disable donation. Typed bindings also retain their descriptor/scalar
borrow until dispatch. Early failures keep ownership with Rust; registration
transfers it even when a submitted standalone command later reports failure.
No shader, flush, commit, wait or additional completion-handler change.
Normal and unretained collections pass 45/45 tests including native allocation,
heap exhaustion, limits, residency and in-flight cache exclusion. This is not model parity.
Binding input/output roles, barrier epochs, concurrent contexts, inter-encoder
fences and reference commit thresholds still require the full encoder port.
Runtime-generated indexing shader
--------------------------------
The same pinned runtime JIT-compiles gather_front instead of using the
precompiled library. tools/mtplx-kernel-source.py resolves its two headers
into metal/mtplx_qwen.metal without editing shader code and supplies the
BF16/U32/FP32 template instantiations. Original notices and MIT attribution
are retained. Header identities:
indexing/indexing.h:
e820b8ee2b5132a97122780c12433ebb5100d8078d31e211d0429400a11415bb
indexing/gather_front.h:
64aacebf6576dfcd389383564fa1214bc87f2a091dd33cc64c598c5367ecab96
The generator requires the pinned runtime source checkout alongside the
MTPLX reference checkout, named mtplx-runtime-0.32.2. The product requires
only the generated shader resource, not this reference source checkout.
Runtime-generated SiLU shader
----------------------------
tools/mtplx-jit-reference.py observes original runtime compilation without
changing it. tests/fixtures/mtplx-silu-jit.json pins the generated19variants;
full observed source SHA256:
76cafb45db55a91efba66503dde59b37628ea360f220f35dd860f4ac3c3d0111
The shader exporter retains the generated computation and its original
BF16 math, Sigmoid, Multiply, cast and stride helpers. Only host-name aliases
change. All supporting header identities are enforced by the exporter and
the emitted unit hashes by the focused Rust test. Copyright Apple Inc.; MIT
as reproduced in MLX-LM-LICENSE.txt. This does not link a host runtime.
Runtime-generated GatherAxis shader
-----------------------------------
The pinned GatherAxis source and generic elem_to_loc helper are retained
unchanged, with BF16/U32 index instantiations for contiguous/strided inputs
and int/int64 offsets. The router retains the strided last-ten-column view;
no replacement top-k kernel is used. Header identities:
indexing/gather_axis.h:
e1a745391ff4990f3f1ad75c5687c3b102dcdc4833d8fbbac38e10f54af29af4
utils.h:
5e1568e9edde9d05dbf86f68fa0d6c6240f2c32b973c7c6a76166b9c0d91543d
Copyright Apple Inc.; MIT as reproduced in MLX-LM-LICENSE.txt. Softmax,
reduction, binary operations and index copies use the pinned metallib.
Runtime-generated SwiGLU shader
------------------------------
The original compiled activations.swiglu used by non-sanitize-fused SwitchGLU
and Qwen3NextMLP is captured separately from nn.silu. Full observed source SHA:
bf78eee5cf96ea7c112c4e61546c12bcacf57fe512e0572182604cb94137510b.
tests/fixtures/mtplx-swiglu-jit.json preserves all 19 original generated variants.
The exporter changes only host aliases and reuses the already pinned BF16,
Sigmoid, Multiply and cast/stride dependencies. Copyright Apple Inc.; MIT.
Runtime-generated compute_g shader and staged GDN
------------------------------------------------
The original compiled gated_delta.compute_g is captured with the same observer:
tests/fixtures/mtplx-compute_g-jit.json. Full observed source SHA256:
34143a98046f8af5538767734fc169a5cab22a4920c26f9ba7ea45b8097152de.
Receipt SHA256:
701e2f54b7cb8bf97616f83256657e6c5e8cc8b46f4b65c559ccc030ab111dbf.
All 19 variants retain their original BF16 Add/LogAddExp intermediates and
FP32 final exponential. Only host aliases change. The exporter pins the
additional Exp, Negative, Add, LogAddExp, Limits and log1p shader dependencies.
complex.h SHA256:
16e8a815b2cbdb6070e0824e64fe33fccb6e918f1b84ea5c792bd89d33e57bf1.
cexpf.h SHA256:
88b6e15a52a5800d98d9bc6da840ca5cf70bf572fda136409580c1f17b1e0aab.
The complex overload dependencies are retained unchanged, not used to add a
complex-valued Qwen path. complex.h is Apple MIT. cexpf.h is Apache-2.0,
Copyright Apple 2025, NVIDIA 2008-2013 and Filipe RNC Maia 2013. Its full original
copyright/license notice remains embedded in the generated shader; the Apache
license text is included in MTPLX-LICENSE.txt.
Stock depthwise Conv1D, copies, casts, reductions and elementary operations
use the unchanged runtime metallib. Cache valid-length GatherAxis additionally
instantiates the original signed INT32-index template; router indices remain
UINT32. No shader body is replaced by a hand-written equivalent.
Original QSA indexer preparation
-------------------------------
qsa_indexer_prepare.py SHA256:
a77f6ca5ae805e729519c4629ae88b455a6dbf473a457a6e1c8219174eb59091.
The exporter reads _prepare_queries_kernel and _pool_keys_kernel as AST data;
it does not execute the model or kernel module. Both original source strings
are unchanged. Header substitutions match the installed geometry: four query
heads, width128, rotary64, ratio4, epsilon1e-6, attention scaling1. Includes are
resolved at translation-unit scope; separate namespaces avoid collisions among
the original header constants. Only entry-point declarations, host aliases and
template instantiations are adapted. Stride metadata retains the original
constant int64_t address space. Original Metal math and BF16 rounding remain.
Copyright MTPLX; Apache-2.0, see MTPLX-LICENSE.txt and MTPLX-NOTICE.txt.
This is the preparation portion, not the full QSA indexer, selection,
attention graph or production integration. Runtime frequencies are input buffers,
not host replacements for the model's frequency construction.
QSACache/KVCache host lifecycle now uses the original scalar/vector/general
copy and BF16/FP32 cast entries from this runtime, including positional writes,
growth, strided restored state and the derived mirror. Rust distinguishes array
object identity (__setitem__ overwrites its descriptor) from shared slice storage.
Retained state aliases are checked against actual MTPLX cache operations, not
assumed immutable. No additional shader bodies or runtime host library are used.
The connected canonical cache remains test-only until product graph integration.
Original dynamic QSA selector
-----------------------------
qsa_indexer_select.py SHA256:
a3c74af27a7045c12f2893a8b7a91724c00d8a4148315c3165f3480c83016cf3.
metal/mtplx-qsa-select.json preserves the original header, common body and all
three epilogues (blocks, dense_mask, row_tokens), extracted without importing
the model. Rust substitutes the original literal header parameters and supplies
only the entry-point ABI. Tests additionally compare full generated header/body
hashes against the actual MTPLX factory. H4/D128/ratio4 match the installed model;
BF16/FP32 operands, backing capacity, top-k and TF32 remain specializations.
Native compilation follows runtime 0.32.2 CustomKernel defaults: Safe math and
its platform-selected Metal language version. No runtime host library is linked.
The original 32MiB score-scratch chunk planner and typed output concatenation
are connected to the cache/preparation port. General submission/concurrency,
the complete eager indexer and production integration remain open.
Copyright MTPLX; Apache-2.0, see MTPLX-LICENSE.txt and MTPLX-NOTICE.txt.
Original vectorized QSA prefill
------------------------------
qsa_indexer_prefill.py SHA256:
4d6fd428243c001746f69f8aed45991356772c2bd4a45586eb3c6813c91998d3.
The same JSON export retains _MPP_SCORE_HEADER/_MPP_SCORE_SOURCE, the original
top-k body and literal f-string header segments. Rust resolves only their named
constants and provides entry-point ABI/type aliases. TensorOps tile layout,
ordered per-head ReLU reduction, adaptive radix/insertion and all epilogues are
unchanged. The original required General FP32 copy is used for non-contiguous
score views; MPP input views keep their strides without added copies.
The 128MiB producer-aware planner and score -> top-k -> concat chain are connected
for the installed BF16/M5 geometry, including a 2K continuation from live cache.
Full indexer branch routing, compiled graph bank and production integration
remain open. Copyright MTPLX; Apache-2.0.
The general FP32 score expression now shares that prefill entry point. Rust
ports runtime matmul.cpp's H4/D128 M5 Max routing: AsType Vector/General layout,
check_transpose and broadcast copies, batch collapse, GEMV, regular Steel/NAX
and both Split-K variants, original per-head Maximum, row/column Sum and Divide.
All shader entries come from the unchanged pinned metallib; no shader body or
host runtime library was added. The pooled cast is retained once across chunks;
producer selection and the H4+1 workspace budget follow the reference.
384 score cases and 96 connected selection cases are exact with real runtime
MLX_ENABLE_TF32=0/1 in separate processes. These remain canonical correctness
fixtures, not production integration or performance-parity evidence.
Original eager QSA selection
---------------------------
The untiled QSAIndexer._select_eager score/top-k path uses the original
runtime Arange, Add, integer Divide, Less, casts, Select, Subtract and
ArgPartition (implemented by the pinned runtime as argsort). The chronological
flash_prefill block epilogue adds original int32 Sort, int64 index conversion,
bool GatherAxis and Select. The exporter adds only the required
gather_axis<bool,int64_t,int,true,true> instantiation of the already preserved
GatherAxis body; no body is changed. Its original file/unit hashes are unchanged.
All untiled output epilogues are connected: dense mask (original bool
ScatterAxis, repeat/concatenate and causal/tail mask), rows-gather (argsort-order
tokens and validity), decode flash (chronological blocks, host tail bound) and
decode gather (chronological tokens and variable-length tail). The flash branch
retains precedence; neither decode branch evaluates the dead selected-mask DAG.
Shared original cast/sort/vector dispatch helpers do not alter the shader bodies.
ScatterAxis adds these unchanged pinned Apple MIT runtime source units:
- atomic.h, full-file SHA256:
4c35ea2798a2335502865247aee878149fc9ada0d7e84c05d771baef0c7fcc60
- reduction/ops.h None operation, full-file SHA256:
78d06730fc9564a73944e7f1fe3897d25c8789b28a939bf418e1968db311da41
- indexing/scatter_axis.h, full-file SHA256:
43eabd0216101f8e32f5cdd19ce40b7f954564be27fad98a5e0fe345e7b94ce5
Only include/pragma-once placement, namespace and the two required
scatter_axis<bool,int64_t,int,None,false/true,true> instantiations are added
outside the preserved bodies. The exporter guards full-file and body hashes.
No host C/C++ runtime implementation is linked.
The 408 eager receipts tap actual QSAIndexer calls: 60 score/top-k, 24 prefill
blocks, 144 dense masks, 108 rows-gather and 72 decode outputs. They include 2K
queries, 65,536 blocks, tails 0/1/3 and separate real TF32-on/off processes.
The tiled path shares the same score/rank functions, pooled FP32 input and tie
vector. Each original mx.eval(top_t) is a synchronous command completion before
the next tile, not an asynchronous flush. Only evaluated index views/backings
are retained through the original GeneralGeneral uint32 concatenate. Index
stride changes N -> K, independently of the N-strided validity. No new shader
source or specialization is needed. Rust shares the original output-branch
priority, including the tiled rows-gather exclusion and decode flash precedence.
132 further actual-call receipts (108 tiled, 24 tile-off boundaries) bring the
eager total to 540. They include observed reference eval row counts, 2K queries,
65,536 blocks, tail 0/1, partial tiles and stride-2 FP32 query views.
The existing GPU busy counters additionally verify one completed command buffer
per observed reference tile eval, plus the final output batch.
Full indexer routing, the general scheduling tape/allocator and production
integration are not established by these checks.
Original eager QSA preparation
------------------------------
The installed BF16 H4/D128 query and H1/D128 pool paths now use the original
eager preparation expression as well as the fused custom-kernel branch.
RMSNorm uses the pinned runtime kernel and its required General-Copy for sliced
projection inputs. Pool mean is FP32 sum multiplied by 0.25 and cast to BF16
before weighted RMSNorm. RoPE preserves all Arange, casts, concatenations,
Cos/Sin, BF16 Negative, FP32 Multiply/Add and final BF16/pass-through stages.
The installed rotary64/ratio4/eps1e-6/scaling1 contract is unchanged.
All entries come from the unchanged pinned metallib; no shader body was added.
The existing projection and cache-extension entry points select either branch
and retain the eager intermediates through their consuming operations. 105
additional actual QSAIndexer receipts cover bare and quantized-projection
preparation, including 2K rows, padded/stride-2 inputs and high positions. Another
44 real QSACache/KVCache transitions cover eager pooling, capacity growth,
reservation, trim, state aliases/restore and FP32 mirror rebuilding. These are
not a complete indexer, production-integration or performance-parity receipt.
Connected non-compiled indexer entry
----------------------------------
The Rust entry after compiled-route rejection now connects projection/supplied
QK views, query preparation, raw/pool state and the original large-prefill,
legacy-fused and eager selection order. All existing original shader dispatches
are reused. Query preparation is dead when dense==sparse; KV.offset advances
only in the subsequent Attention step. Shared return variants preserve the
model-visible outputs while retaining other encoded kernel outputs.
92 actual MTPLX indexer calls in 24 ongoing sequences verify lane choice, 82
selection hashes, 268 raw/pool/mirror hashes, capacities/frontiers and command
completion counts. They cover 2K rows, 32K history, supplied 704-stride QK views,
prefill crossover, gather/flash priority and tiling. No compiled path is silently
replaced. Compiled eligibility/core, attention, graph scheduling and production
integration are still open; these are correctness, not performance receipts.
+478 -408
View File
@@ -197,33 +197,6 @@ kernel void kernel_mul_mv_q8_0_f32(
kernel_mul_mv_q8_0_f32_impl<N_R0_Q8_0, constant ds4_metal_args_mul_mv &>(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg); kernel_mul_mv_q8_0_f32_impl<N_R0_Q8_0, constant ds4_metal_args_mul_mv &>(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg);
} }
[[host_name("kernel_mul_mv_q8_0_f32_r4")]]
kernel void kernel_mul_mv_q8_0_f32_r4(
constant ds4_metal_args_mul_mv & args,
device const char * src0,
device const char * src1,
device char * dst,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
kernel_mul_mv_q8_0_f32_impl<4, constant ds4_metal_args_mul_mv &>(args, src0, src1, dst, shmem, tgpig, tiisg, sgitg);
}
// Output projection alias used by the optimized host dispatch.
[[host_name("kernel_mul_mv_q8_0_f32_nr4")]]
kernel void kernel_mul_mv_q8_0_f32_nr4(
constant ds4_metal_args_mul_mv & args,
device const char * src0,
device const char * src1,
device char * dst,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
kernel_mul_mv_q8_0_f32_impl<4, constant ds4_metal_args_mul_mv &>(
args, src0, src1, dst, shmem, tgpig, tiisg, sgitg);
}
// Decode Q-A/KV pair. Both projections consume the same activation row but // Decode Q-A/KV pair. Both projections consume the same activation row but
// have independent weight ranges and output extents. Keep the standalone Q8_0 // have independent weight ranges and output extents. Keep the standalone Q8_0
@@ -497,25 +470,190 @@ kernel void kernel_dsv4_shared_gate_up_swiglu_q8_0(
clamp_value, shmem, tgpig, tiisg, sgitg); clamp_value, shmem, tgpig, tiisg, sgitg);
} }
[[host_name("kernel_dsv4_shared_gate_up_swiglu_q8_0_r4")]] // Decode-only fusion of the router logits matvec (F16, embd -> n_expert)
kernel void kernel_dsv4_shared_gate_up_swiglu_q8_0_r4( // with the shared-expert gate/up SwiGLU (Q8_0, embd -> shared). Both read
// the same normalized FFN input back to back; one dispatch removes one
// launch per decode layer. Router threadgroups replicate
// kernel_mul_mv_f16_f32_4 (nsg=8, nr0=2); shared threadgroups host two
// virtual 4-simdgroup cohorts replicating
// kernel_dsv4_shared_gate_up_swiglu_q8_0 (nsg=4, nr0=2), including its
// per-row simd/shmem reduction trees. Bit-exact by construction.
kernel void kernel_dsv4_router_shared_gate_up_q8_0(
constant ds4_metal_args_mul_mv & args, constant ds4_metal_args_mul_mv & args,
constant ds4_metal_args_mul_mv & sargs,
device const char * src0_router,
device const char * src0_gate, device const char * src0_gate,
device const char * src0_up, device const char * src0_up,
device const char * src1, device const char * src1,
device char * dst_router,
device char * dst_gate, device char * dst_gate,
device char * dst_up, device char * dst_up,
device char * dst_mid, device char * dst_mid,
constant float &clamp_value, constant float &clamp_value,
threadgroup char * shmem [[threadgroup(0)]], threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]], uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiisg[[thread_index_in_simdgroup]], ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) { ushort sgitg [[simdgroup_index_in_threadgroup]]) {
kernel_dsv4_shared_gate_up_swiglu_q8_0_impl<4, true>( constexpr short NW = N_SIMDWIDTH;
args, src0_gate, src0_up, src1, dst_gate, dst_up, dst_mid, const uint router_tgs = ((uint)args.ne01 + 1u) / 2u;
clamp_value, shmem, tgpig, tiisg, sgitg);
if (tgpig.x < router_tgs) {
// Exact replica of kernel_mul_mv_f16_f32_4 with NSG=8, NR0=2.
constexpr short NSG = 8;
constexpr short NR0 = 2;
constexpr short NB = 32;
constexpr short NF = 16;
constexpr short NF4 = NF/4;
const int nb = args.ne00/NB;
const int r0 = tgpig.x*NR0;
device const float4 * y4 = (device const float4 *) src1;
device const half4 * ax4[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
ax4[row] = (device const half4 *)
(src0_router + (uint64_t)(r0 + row)*args.nb01);
}
float sumf[NR0] = { 0.f };
const short ix = tiisg/(NW/NF);
const short il = tiisg%(NW/NF);
const int ib0 = sgitg*NF + ix;
device const float4 * yb4 = y4 + (ib0*NB + il*NF)/4;
for (int ib = ib0; ib < nb; ib += NSG*NF) {
float4 yl4[NF4];
FOR_UNROLL (short i = 0; i < NF4; ++i) {
yl4[i] = yb4[i];
}
FOR_UNROLL (short row = 0; row < NR0; row++) {
device const half4 * xb4 = ax4[row] + (ib*NB + il*NF)/4;
float sumq = 0.f;
FOR_UNROLL (short i = 0; i < NF4; ++i) {
sumq += dot(float4(xb4[i]), yl4[i]);
}
sumf[row] += sumq;
}
yb4 += NSG*NF*NW/4;
}
device float * dst_f32 = (device float *) dst_router;
helper_mv_reduce_and_write<NR0>(dst_f32, sumf, r0, args.ne01,
tiisg, sgitg, shmem);
return;
}
// Shared-expert part: two virtual nsg=4 cohorts per threadgroup, each an
// exact replica of kernel_dsv4_shared_gate_up_swiglu_q8_0 (NR0=2).
constexpr short NSG = 4;
constexpr short NR0 = 2;
constexpr short NQ = 8;
const uint cohort = sgitg >> 2;
const ushort vsg = sgitg & 3u;
const uint vt = (tgpig.x - router_tgs) * 2u + cohort;
const int nb = sargs.ne00 / QK8_0;
const int r0 = vt * NR0;
device const float *y = (device const float *) src1;
device const block_q8_0 *ag[NR0];
device const block_q8_0 *au[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
const uint64_t offset0 = (uint64_t)(r0 + row) * sargs.nb01;
ag[row] = (device const block_q8_0 *)(src0_gate + offset0);
au[row] = (device const block_q8_0 *)(src0_up + offset0);
}
float sumg[NR0] = { 0.f };
float sumu[NR0] = { 0.f };
const short ix = tiisg / (NW / NQ);
const short il = tiisg % (NW / NQ);
const int ib0 = vsg * NQ + ix;
float yl[NQ];
device const float *yb = y + ib0 * QK8_0 + il * NQ;
for (int ib = ib0; ib < nb; ib += NSG * NQ) {
FOR_UNROLL (short i = 0; i < NQ; ++i) {
yl[i] = yb[i];
}
FOR_UNROLL (short row = 0; row < NR0; ++row) {
device const int8_t *qg = ag[row][ib].qs + il * NQ;
device const int8_t *qu = au[row][ib].qs + il * NQ;
float sg = 0.f;
float su = 0.f;
FOR_UNROLL (short i = 0; i < NQ; ++i) {
sg += qg[i] * yl[i];
su += qu[i] * yl[i];
}
sumg[row] += sg * ag[row][ib].d;
sumu[row] += su * au[row][ib].d;
}
yb += NSG * NQ * QK8_0;
}
threadgroup float *shmem_f32 = (threadgroup float *)shmem + cohort * (2*NR0*NW);
threadgroup float *sh_gate[NR0];
threadgroup float *sh_up[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
sh_gate[row] = shmem_f32 + NW * row;
sh_up[row] = shmem_f32 + NW * (NR0 + row);
if (vsg == 0) {
sh_gate[row][tiisg] = 0.0f;
sh_up[row][tiisg] = 0.0f;
}
sumg[row] = simd_sum(sumg[row]);
sumu[row] = simd_sum(sumu[row]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL (short row = 0; row < NR0; ++row) {
if (tiisg == 0) {
sh_gate[row][vsg] = sumg[row];
sh_up[row][vsg] = sumu[row];
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
device float *gate_f32 = (device float *)dst_gate;
device float *up_f32 = (device float *)dst_up;
device float *mid_f32 = (device float *)dst_mid;
FOR_UNROLL (short row = 0; row < NR0 && r0 + row < sargs.ne01; ++row) {
const float gate = simd_sum(sh_gate[row][tiisg]);
const float up = simd_sum(sh_up[row][tiisg]);
if (tiisg == 0 && vsg == 0) {
const uint out_row = r0 + row;
gate_f32[out_row] = gate;
up_f32[out_row] = up;
float g = gate;
float u = up;
if (clamp_value > 1.0e-6f) {
g = min(g, clamp_value);
u = clamp(u, -clamp_value, clamp_value);
}
const float silu = g / (1.0f + exp(-g));
mid_f32[out_row] = silu * u;
}
}
} }
[[host_name("kernel_dsv4_shared_mid_swiglu_q8_0")]] [[host_name("kernel_dsv4_shared_mid_swiglu_q8_0")]]
kernel void kernel_dsv4_shared_mid_swiglu_q8_0( kernel void kernel_dsv4_shared_mid_swiglu_q8_0(
constant ds4_metal_args_mul_mv & args, constant ds4_metal_args_mul_mv & args,
@@ -535,24 +673,6 @@ kernel void kernel_dsv4_shared_mid_swiglu_q8_0(
clamp_value, shmem, tgpig, tiisg, sgitg); clamp_value, shmem, tgpig, tiisg, sgitg);
} }
[[host_name("kernel_dsv4_shared_mid_swiglu_q8_0_r4")]]
kernel void kernel_dsv4_shared_mid_swiglu_q8_0_r4(
constant ds4_metal_args_mul_mv & args,
device const char * src0_gate,
device const char * src0_up,
device const char * src1,
device char * dst_gate,
device char * dst_up,
device char * dst_mid,
constant float &clamp_value,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
kernel_dsv4_shared_gate_up_swiglu_q8_0_impl<4, false>(
args, src0_gate, src0_up, src1, dst_gate, dst_up, dst_mid,
clamp_value, shmem, tgpig, tiisg, sgitg);
}
template<typename T0, typename T1, short NR0, typename args_t> template<typename T0, typename T1, short NR0, typename args_t>
void kernel_mul_mv_t_t_impl( void kernel_mul_mv_t_t_impl(
@@ -974,6 +1094,311 @@ kernel void kernel_mul_mv_f16_f32_pair_compressor_store_4(
state_score[dst] = projected_score[col] + ape_v; state_score[dst] = projected_score[col] + ape_v;
} }
// Decode compressor + indexer-compressor projection in one dispatch. Both
// pairs read the same normalized activation with the same F16 matvec shape,
// so one launch covers all four matrices: threadgroups below the first
// range boundary run the exact paired matvec + state store of
// kernel_mul_mv_f16_f32_pair_compressor_store_4 for the attention
// compressor, the rest for the indexer compressor. Per-row reduction trees
// and the per-threadgroup state stores are unchanged, keeping the fused
// result bit-identical to the two separate dispatches while removing one
// dispatch per decode layer.
kernel void kernel_mul_mv_f16_f32_quad_compressor_store_4(
constant ds4_metal_args_mul_mv & args,
constant ds4_metal_args_compressor_pair_store & store0,
constant ds4_metal_args_compressor_pair_store & store1,
device const char * src0_a0,
device const char * src0_b0,
device const char * src0_a1,
device const char * src0_b1,
device const char * src1,
device char * dst_a0,
device char * dst_b0,
device char * dst_a1,
device char * dst_b1,
device const char * ape0,
device const char * ape1,
device float * state0_kv,
device float * state0_score,
device float * state1_kv,
device float * state1_score,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiitg [[thread_index_in_threadgroup]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
constexpr short NR0 = 2;
const uint tgs0 = ((uint)store0.width + NR0 - 1u) / NR0;
const bool second = tgpig.x >= tgs0;
uint3 local_tgpig = tgpig;
if (second) local_tgpig.x = tgpig.x - tgs0;
ds4_metal_args_mul_mv largs = args;
largs.nr0 = NR0;
largs.ne01 = second ? (int32_t)store1.width : (int32_t)store0.width;
if (!second) {
kernel_mul_mv_f16_f32_pair_4_impl<NR0>(
largs, src0_a0, src0_b0, src1, dst_a0, dst_b0,
shmem, local_tgpig, tiisg, sgitg);
} else {
kernel_mul_mv_f16_f32_pair_4_impl<NR0>(
largs, src0_a1, src0_b1, src1, dst_a1, dst_b1,
shmem, local_tgpig, tiisg, sgitg);
}
threadgroup_barrier(mem_flags::mem_device);
// State append: identical to the paired store kernel, scoped to the
// range this threadgroup just projected (its own outputs only).
constant ds4_metal_args_compressor_pair_store & store = second ? store1 : store0;
if (tiitg >= NR0 || store.width == 0u || store.ratio == 0u) {
return;
}
const uint col = local_tgpig.x * (uint)NR0 + tiitg;
if (col >= store.width) return;
const uint pos_mod = store.pos % store.ratio;
const uint dst_row = store.ratio == 4u ? store.ratio + pos_mod : pos_mod;
const uint dst = dst_row * store.width + col;
const uint ape_i = pos_mod * store.width + col;
device volatile const float * projected_kv = second
? (device volatile const float *)dst_a1
: (device volatile const float *)dst_a0;
device volatile const float * projected_score = second
? (device volatile const float *)dst_b1
: (device volatile const float *)dst_b0;
device const char * ape = second ? ape1 : ape0;
device float * state_kv = second ? state1_kv : state0_kv;
device float * state_score = second ? state1_score : state0_score;
float ape_v;
if (store.ape_type == 1u) {
ape_v = (float)(((device const half *)ape)[ape_i]);
} else {
ape_v = ((device const float *)ape)[ape_i];
}
state_kv[dst] = projected_kv[col];
state_score[dst] = projected_score[col] + ape_v;
}
/* Decode-only fusion: one dispatch covers the q_a/kv Q8 pair projection and
* the four F16 compressor projections (attention + indexer) with their
* state-store epilogue. Both stages read the same normalized attention
* input and write disjoint outputs. The q_a/kv range hosts two virtual
* NSG=4 cohorts per threadgroup, each an exact replica of
* kernel_mul_mv_q8_0_f32_pair (same per-lane K walk and reduction tree, cf.
* kernel_dsv4_router_shared_gate_up_q8_0); the compressor ranges run
* kernel_mul_mv_f16_f32_pair_4_impl<2> and the paired store epilogue
* verbatim, so every output bit matches the two separate dispatches. */
kernel void kernel_dsv4_qkv_pair_quad_compressor_store_q8_0(
constant ds4_metal_args_mul_mv & args0,
constant ds4_metal_args_mul_mv & args1,
constant ds4_metal_args_mul_mv & cargs,
constant ds4_metal_args_compressor_pair_store & store0,
constant ds4_metal_args_compressor_pair_store & store1,
constant uint & pair_vtgs,
device const char * qw0,
device const char * qw1,
device const char * cw0a,
device const char * cw0b,
device const char * cw1a,
device const char * cw1b,
device const char * src1,
device char * dst0,
device char * dst1,
device char * cdst_a0,
device char * cdst_b0,
device char * cdst_a1,
device char * cdst_b1,
device const char * ape0,
device const char * ape1,
device float * state0_kv,
device float * state0_score,
device float * state1_kv,
device float * state1_score,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiitg [[thread_index_in_threadgroup]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
constexpr short NW = N_SIMDWIDTH;
const uint pair_ctgs = (pair_vtgs + 1u) / 2u;
if (tgpig.x < pair_ctgs) {
/* Q8 pair range: cohort c of threadgroup t runs virtual pair
* threadgroup 2t+c with the original NSG=4 mapping. */
constexpr short NSG = 4;
constexpr short NQ = 8;
constexpr short NR0 = 2;
const uint cohort = sgitg >> 2;
const ushort vsg = sgitg & 3u;
const uint vt = tgpig.x * 2u + cohort;
const bool valid = vt < pair_vtgs;
const int r0 = vt * NR0;
const bool active_a = valid && r0 < args0.ne01;
const bool active_b = valid && r0 < args1.ne01;
const int nb = args0.ne00 / QK8_0;
device const float *y = (device const float *)src1;
device const block_q8_0 *ax_a[NR0];
device const block_q8_0 *ax_b[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
const int out_row = r0 + row;
ax_a[row] = active_a && out_row < args0.ne01
? (device const block_q8_0 *)(qw0 + (uint64_t)out_row * args0.nb01)
: (device const block_q8_0 *)qw0;
ax_b[row] = active_b && out_row < args1.ne01
? (device const block_q8_0 *)(qw1 + (uint64_t)out_row * args1.nb01)
: (device const block_q8_0 *)qw1;
}
float suma[NR0] = { 0.f };
float sumb[NR0] = { 0.f };
const short ix = tiisg / (NW / NQ);
const short il = tiisg % (NW / NQ);
const int ib0 = vsg * NQ + ix;
float yl[NQ];
device const float *yb = y + ib0 * QK8_0 + il * NQ;
if (valid) {
for (int ib = ib0; ib < nb; ib += NSG * NQ) {
FOR_UNROLL (short i = 0; i < NQ; ++i) {
yl[i] = yb[i];
}
FOR_UNROLL (short row = 0; row < NR0; ++row) {
const int out_row = r0 + row;
if (active_a && out_row < args0.ne01) {
device const int8_t *qs = ax_a[row][ib].qs + il * NQ;
float sumq = 0.f;
FOR_UNROLL (short i = 0; i < NQ; ++i) {
sumq += qs[i] * yl[i];
}
suma[row] += sumq * ax_a[row][ib].d;
}
if (active_b && out_row < args1.ne01) {
device const int8_t *qs = ax_b[row][ib].qs + il * NQ;
float sumq = 0.f;
FOR_UNROLL (short i = 0; i < NQ; ++i) {
sumq += qs[i] * yl[i];
}
sumb[row] += sumq * ax_b[row][ib].d;
}
}
yb += NSG * NQ * QK8_0;
}
}
threadgroup float *shared =
(threadgroup float *)shmem + cohort * (2 * NR0 * NW);
threadgroup float *sha[NR0];
threadgroup float *shb[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
sha[row] = shared + NW * row;
shb[row] = shared + NW * (NR0 + row);
if (vsg == 0) {
sha[row][tiisg] = 0.0f;
if (active_b) shb[row][tiisg] = 0.0f;
}
suma[row] = simd_sum(suma[row]);
if (active_b) sumb[row] = simd_sum(sumb[row]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL (short row = 0; row < NR0; ++row) {
if (tiisg == 0) {
sha[row][vsg] = suma[row];
if (active_b) shb[row][vsg] = sumb[row];
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
device float *out_a = (device float *)dst0;
device float *out_b = (device float *)dst1;
FOR_UNROLL (short row = 0; row < NR0; ++row) {
const float total_a = simd_sum(sha[row][tiisg]);
if (tiisg == 0 && vsg == 0) {
const int out_row = r0 + row;
if (active_a && out_row < args0.ne01) out_a[out_row] = total_a;
}
if (active_b) {
const float total_b = simd_sum(shb[row][tiisg]);
if (tiisg == 0 && vsg == 0) {
const int out_row = r0 + row;
if (out_row < args1.ne01) out_b[out_row] = total_b;
}
}
}
return;
}
/* Compressor quad range: verbatim body of
* kernel_mul_mv_f16_f32_quad_compressor_store_4 on the shifted grid. */
constexpr short NR0 = 2;
const uint lx = tgpig.x - pair_ctgs;
const uint tgs0 = ((uint)store0.width + NR0 - 1u) / NR0;
const bool second = lx >= tgs0;
uint3 local_tgpig = tgpig;
local_tgpig.x = second ? lx - tgs0 : lx;
ds4_metal_args_mul_mv largs = cargs;
largs.nr0 = NR0;
largs.ne01 = second ? (int32_t)store1.width : (int32_t)store0.width;
if (!second) {
kernel_mul_mv_f16_f32_pair_4_impl<NR0>(
largs, cw0a, cw0b, src1, cdst_a0, cdst_b0,
shmem, local_tgpig, tiisg, sgitg);
} else {
kernel_mul_mv_f16_f32_pair_4_impl<NR0>(
largs, cw1a, cw1b, src1, cdst_a1, cdst_b1,
shmem, local_tgpig, tiisg, sgitg);
}
threadgroup_barrier(mem_flags::mem_device);
// State append: identical to the paired store kernel, scoped to the
// range this threadgroup just projected (its own outputs only).
constant ds4_metal_args_compressor_pair_store & store = second ? store1 : store0;
if (tiitg >= NR0 || store.width == 0u || store.ratio == 0u) {
return;
}
const uint col = local_tgpig.x * (uint)NR0 + tiitg;
if (col >= store.width) return;
const uint pos_mod = store.pos % store.ratio;
const uint dst_row = store.ratio == 4u ? store.ratio + pos_mod : pos_mod;
const uint dst = dst_row * store.width + col;
const uint ape_i = pos_mod * store.width + col;
device volatile const float * projected_kv = second
? (device volatile const float *)cdst_a1
: (device volatile const float *)cdst_a0;
device volatile const float * projected_score = second
? (device volatile const float *)cdst_b1
: (device volatile const float *)cdst_b0;
device const char * ape = second ? ape1 : ape0;
device float * state_kv = second ? state1_kv : state0_kv;
device float * state_score = second ? state1_score : state0_score;
float ape_v;
if (store.ape_type == 1u) {
ape_v = (float)(((device const half *)ape)[ape_i]);
} else {
ape_v = ((device const float *)ape)[ape_i];
}
state_kv[dst] = projected_kv[col];
state_score[dst] = projected_score[col] + ape_v;
}
template<typename T0, typename T1, typename args_t> template<typename T0, typename T1, typename args_t>
void kernel_mul_mv_t_t_short_impl( void kernel_mul_mv_t_t_short_impl(
args_t args, args_t args,
@@ -1476,125 +1901,6 @@ constant bool FC_mul_mm_bc_inp [[function_constant(FC_MUL_MM + 0)]];
constant bool FC_mul_mm_bc_out [[function_constant(FC_MUL_MM + 1)]]; constant bool FC_mul_mm_bc_out [[function_constant(FC_MUL_MM + 1)]];
#ifdef DS4_METAL_HAS_TENSOR #ifdef DS4_METAL_HAS_TENSOR
template<
short NR0, short NR1,
typename SA, typename SA_4x4, typename block_q, short nl,
void (*dequantize_func)(device const block_q *, short, thread SA_4x4 &),
typename T0, typename T0_4x4, typename T1>
kernel void kernel_mul_mm_mpp(
constant ds4_metal_args_mul_mm & args,
device const char * srcA,
device const char * srcB,
device char * dst,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiitg [[thread_index_in_threadgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
(void) sgitg;
constexpr int NK = 32;
constexpr int NL = NK/16;
constexpr int NUM_THREADS = 128;
const int K = args.ne00;
const int M = args.ne0;
const int N = args.ne1;
const int im = tgpig.z;
const int i12 = im%args.ne12;
const int i13 = im/args.ne12;
const int r0 = tgpig.y*NR0;
const int r1 = tgpig.x*NR1;
const uint64_t offset0 = (i12/args.r2)*args.nb02 + (i13/args.r3)*args.nb03;
threadgroup SA *sa = (threadgroup SA *)shmem;
threadgroup SA *sb = sa + NR0*NK;
auto tA = tensor(sa, dextents<int32_t, 2>(NK, NR0));
auto tB = tensor(sb, dextents<int32_t, 2>(NK, NR1));
device const T1 *ptrB = (device const T1 *)(srcB + args.nb12*i12 + args.nb13*i13);
const int strideB = args.nb11/sizeof(T1);
matmul2d<
matmul2d_descriptor(NR1, NR0, NK, false, true, false,
matmul2d_descriptor::mode::multiply_accumulate),
execution_simdgroups<4>> mm;
auto cT = mm.template get_destination_cooperative_tensor<decltype(tB), decltype(tA), float>();
#pragma unroll
for (uint16_t i = 0; i < cT.get_capacity(); ++i) {
if (cT.is_valid_element(i)) {
cT[i] = 0.0f;
}
}
for (int loop_k = 0; loop_k < K; loop_k += NK) {
for (int work = tiitg; work < NR0*NL; work += NUM_THREADS) {
const int row = work/NL;
const int k_chunk = work%NL;
const int k_pos = loop_k + k_chunk*16;
const short k_base = k_chunk*16;
if (!FC_mul_mm_bc_out || r0 + row < M) {
if (is_same<T0_4x4, block_q>::value && FC_mul_mm_bc_inp) {
device const T0 *row_ptr = (device const T0 *)(srcA + args.nb01*(r0 + row) + offset0);
FOR_UNROLL (short i = 0; i < 16; i++) {
sa[row*NK + k_base + i] = (k_pos + i < K) ? (SA)row_ptr[k_pos + i] : (SA)0;
}
} else {
const int block_idx = k_pos/(16*nl);
const short il = (k_pos/16)%nl;
device const block_q *row_ptr = (device const block_q *)(srcA + args.nb01*(r0 + row) + offset0);
SA_4x4 temp_a;
dequantize_func(row_ptr + block_idx, il, temp_a);
FOR_UNROLL (short i = 0; i < 16; i++) {
sa[row*NK + k_base + i] = (k_pos + i < K) ? temp_a[i/4][i%4] : (SA)0;
}
}
} else {
FOR_UNROLL (short i = 0; i < 16; i++) {
sa[row*NK + k_base + i] = (SA)0;
}
}
}
for (int work = tiitg; work < NK*NR1; work += NUM_THREADS) {
const int col = work/NK;
const int k = work%NK;
if ((!FC_mul_mm_bc_out && !FC_mul_mm_bc_inp) ||
(r1 + col < N && loop_k + k < K)) {
sb[col*NK + k] = (SA)ptrB[(uint64_t)(r1 + col)*strideB + loop_k + k];
} else {
sb[col*NK + k] = (SA)0;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
auto mA = tA.slice(0, 0);
auto mB = tB.slice(0, 0);
mm.run(mB, mA, cT);
threadgroup_barrier(mem_flags::mem_threadgroup);
}
device float *dst_batch = (device float *)dst + im*N*M;
if (!FC_mul_mm_bc_out) {
device float *dst_tile = dst_batch + r0 + (uint64_t)r1*M;
auto tD = tensor(dst_tile, dextents<int32_t, 2>(NR0, NR1), array<int, 2>({1, M}));
cT.store(tD);
} else {
auto tD = tensor(dst_batch, dextents<int32_t, 2>(M, N), array<int, 2>({1, M}));
auto mD = tD.slice(r0, r1);
cT.store(mD);
}
}
typedef decltype(kernel_mul_mm_mpp<64, 32, half, half4x4, float4x4, 1, dequantize_f32, float, float4x4, float>) mul_mm_mpp_t;
template [[host_name("kernel_mul_mm_f16_f32_mpp")]] kernel mul_mm_mpp_t kernel_mul_mm_mpp<64, 32, half, half4x4, half4x4, 1, dequantize_f16, half, half4x4, float>;
// Retained Metal4/TensorOps dense prefill kernel. The legacy MPP prototype // Retained Metal4/TensorOps dense prefill kernel. The legacy MPP prototype
// staged both operands in threadgroup memory; this version stages only the // staged both operands in threadgroup memory; this version stages only the
// model weight tile and lets MPP read the dense RHS activation matrix directly // model weight tile and lets MPP read the dense RHS activation matrix directly
@@ -2144,242 +2450,6 @@ kernel void kernel_mul_mm_f16_f32_scaled(
} }
} }
kernel void kernel_mul_mm_f16_f32_pair(
constant ds4_metal_args_mul_mm & args,
device const char * src0_a,
device const char * src0_b,
device const char * src1,
device char * dst_a,
device char * dst_b,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]],
ushort tiitg[[thread_index_in_threadgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
threadgroup half * sa_a = (threadgroup half *)(shmem);
threadgroup half * sa_b = (threadgroup half *)(shmem + 4096);
threadgroup half * sb = (threadgroup half *)(shmem + 8192);
constexpr int NR0 = 64;
constexpr int NR1 = 32;
constexpr int NK = 32;
constexpr int NL0 = NK/16;
constexpr int NL1 = NK/8;
const int im = tgpig.z;
const int r0 = tgpig.y*NR0;
const int r1 = tgpig.x*NR1;
const short nr0 = (args.ne0 - r0 < NR0) ? (args.ne0 - r0) : NR0;
const short nr1 = (args.ne1 - r1 < NR1) ? (args.ne1 - r1) : NR1;
const short lr0 = ((short)tiitg/NL0) < nr0 ? ((short)tiitg/NL0) : nr0 - 1;
const short lr1 = ((short)tiitg/NL1) < nr1 ? ((short)tiitg/NL1) : nr1 - 1;
const short il0 = (tiitg % NL0);
short il = il0;
const int i12 = im%args.ne12;
const int i13 = im/args.ne12;
const uint64_t offset0 = (i12/args.r2)*args.nb02 + (i13/args.r3)*args.nb03;
const short offset1 = il0;
device const half4x4 * xa = (device const half4x4 *)(src0_a + args.nb01*(r0 + lr0) + offset0) + offset1;
device const half4x4 * xb = (device const half4x4 *)(src0_b + args.nb01*(r0 + lr0) + offset0) + offset1;
const short iy = 8*(tiitg % NL1);
device const float * y = (device const float *)(src1
+ args.nb13*i13
+ args.nb12*i12
+ args.nb11*(r1 + lr1)
+ args.nb10*iy);
simdgroup_half8x8 ma[4];
simdgroup_half8x8 mb[2];
simdgroup_float8x8 mc_a[8];
simdgroup_float8x8 mc_b[8];
for (short i = 0; i < 8; i++) {
mc_a[i] = make_filled_simdgroup_matrix<float, 8>(0.f);
mc_b[i] = make_filled_simdgroup_matrix<float, 8>(0.f);
}
for (int loop_k = 0; loop_k < args.ne00; loop_k += NK) {
half4x4 temp_a;
half4x4 temp_b;
dequantize_f16(xa, il, temp_a);
dequantize_f16(xb, il, temp_b);
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL (short i = 0; i < 16; i++) {
const short sx = 2*il0 + i/8;
const short sy = (tiitg/NL0)/8;
const short lx = (tiitg/NL0)%8;
const short ly = i%8;
const short ib = 8*sx + sy;
*(sa_a + 64*ib + 8*ly + lx) = temp_a[i/4][i%4];
*(sa_b + 64*ib + 8*ly + lx) = temp_b[i/4][i%4];
}
if (FC_mul_mm_bc_inp) {
for (short i = 0; i < 8; ++i) {
const short sx = (tiitg%NL1);
const short sy = (tiitg/NL1)/8;
const short lx = i;
const short ly = (tiitg/NL1)%8;
const short ib = 4*sx + sy;
*(sb + 64*ib + 8*ly + lx) = loop_k + iy + i < args.ne00 ? (half) *((device float *) y + i) : 0;
}
} else {
const short sx = (tiitg%NL1);
const short sy = (tiitg/NL1)/8;
const short ly = (tiitg/NL1)%8;
const short ib = 4*sx + sy;
*(threadgroup half2x4 *)(sb + 64*ib + 8*ly) = (half2x4)(*((device float2x4 *) y));
}
il = (il + 2 < 1) ? il + 2 : il % 2;
xa = (il < 2) ? xa + 2 : xa;
xb = (il < 2) ? xb + 2 : xb;
y += NK;
threadgroup_barrier(mem_flags::mem_threadgroup);
threadgroup const half * lsma_a = (sa_a + 4*64*(sgitg%2));
threadgroup const half * lsma_b = (sa_b + 4*64*(sgitg%2));
threadgroup const half * lsmb = (sb + 2*64*(sgitg/2));
FOR_UNROLL (short ik = 0; ik < NK/8; ik++) {
simdgroup_barrier(mem_flags::mem_none);
FOR_UNROLL (short i = 0; i < 2; i++) {
simdgroup_load(mb[i], lsmb + 64*i, 8, 0, false);
}
simdgroup_barrier(mem_flags::mem_none);
FOR_UNROLL (short i = 0; i < 4; i++) {
simdgroup_load(ma[i], lsma_a + 64*i, 8, 0, false);
}
simdgroup_barrier(mem_flags::mem_none);
FOR_UNROLL (short i = 0; i < 8; i++) {
simdgroup_multiply_accumulate(mc_a[i], mb[i/4], ma[i%4], mc_a[i]);
}
simdgroup_barrier(mem_flags::mem_none);
FOR_UNROLL (short i = 0; i < 4; i++) {
simdgroup_load(ma[i], lsma_b + 64*i, 8, 0, false);
}
simdgroup_barrier(mem_flags::mem_none);
FOR_UNROLL (short i = 0; i < 8; i++) {
simdgroup_multiply_accumulate(mc_b[i], mb[i/4], ma[i%4], mc_b[i]);
}
lsma_a += 8*64;
lsma_b += 8*64;
lsmb += 4*64;
}
}
if (!FC_mul_mm_bc_out || (r0 + NR0 <= args.ne0 && r1 + NR1 <= args.ne1)) {
device float * C_a = (device float *) dst_a +
(r0 + 32*(sgitg & 1)) +
(r1 + 16*(sgitg >> 1)) * args.ne0 + im*args.ne1*args.ne0;
device float * C_b = (device float *) dst_b +
(r0 + 32*(sgitg & 1)) +
(r1 + 16*(sgitg >> 1)) * args.ne0 + im*args.ne1*args.ne0;
for (short i = 0; i < 8; i++) {
simdgroup_store(mc_a[i], C_a + 8*(i%4) + 8*args.ne0*(i/4), args.ne0, 0, false);
simdgroup_store(mc_b[i], C_b + 8*(i%4) + 8*args.ne0*(i/4), args.ne0, 0, false);
}
} else {
threadgroup_barrier(mem_flags::mem_threadgroup);
threadgroup float * temp_str = (threadgroup float *) shmem;
for (short i = 0; i < 8; i++) {
simdgroup_store(mc_a[i],
temp_str + 32*(sgitg&1) + (16*(sgitg >> 1))*NR0 + 8*(i%4) + 8*NR0*(i/4),
NR0,
0,
false);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sgitg == 0) {
for (int j = tiitg; j < nr1; j += NR1) {
device float * D = (device float *) dst_a + r0 + (r1 + j)*args.ne0 + im*args.ne1*args.ne0;
device float4 * D4 = (device float4 *) D;
threadgroup float * C = temp_str + (j*NR0);
threadgroup float4 * C4 = (threadgroup float4 *) C;
int i = 0;
for (; i < nr0/4; i++) {
*(D4 + i) = *(C4 + i);
}
i *= 4;
for (; i < nr0; i++) {
*(D + i) = *(C + i);
}
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (short i = 0; i < 8; i++) {
simdgroup_store(mc_b[i],
temp_str + 32*(sgitg&1) + (16*(sgitg >> 1))*NR0 + 8*(i%4) + 8*NR0*(i/4),
NR0,
0,
false);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sgitg == 0) {
for (int j = tiitg; j < nr1; j += NR1) {
device float * D = (device float *) dst_b + r0 + (r1 + j)*args.ne0 + im*args.ne1*args.ne0;
device float4 * D4 = (device float4 *) D;
threadgroup float * C = temp_str + (j*NR0);
threadgroup float4 * C4 = (threadgroup float4 *) C;
int i = 0;
for (; i < nr0/4; i++) {
*(D4 + i) = *(C4 + i);
}
i *= 4;
for (; i < nr0; i++) {
*(D + i) = *(C + i);
}
}
}
}
}
typedef decltype(kernel_mul_mm<half, half4x4, simdgroup_half8x8, half, half2x4, simdgroup_half8x8, float4x4, 1, dequantize_f32, float, float4x4, float, float2x4>) mul_mm_t; typedef decltype(kernel_mul_mm<half, half4x4, simdgroup_half8x8, half, half2x4, simdgroup_half8x8, float4x4, 1, dequantize_f32, float, float4x4, float, float2x4>) mul_mm_t;
// Host-visible prefill matmul variants for F16 and Q8_0 weights. // Host-visible prefill matmul variants for F16 and Q8_0 weights.
+612 -85
View File
@@ -21,20 +21,6 @@ struct ds4_metal_args_dsv4_hc_weighted_sum {
uint64_t nb1; uint64_t nb1;
}; };
struct ds4_metal_args_dsv4_hc_weighted_sum_norm {
int64_t n_embd;
int64_t n_hc;
int64_t n_tokens;
uint64_t nb_x0;
uint64_t nb_x1;
uint64_t nb_x2;
uint64_t nb_w0;
uint64_t nb_w1;
uint64_t nb0;
uint64_t nb1;
uint64_t nb_norm1;
float norm_eps;
};
struct ds4_metal_args_dsv4_output_hc_weights4 { struct ds4_metal_args_dsv4_output_hc_weights4 {
float post_scale; float post_scale;
@@ -411,6 +397,68 @@ kernel void kernel_dsv4_hc_split_weighted_sum(
// kernel_dsv4_hc_split_weighted_sum, stores the HC-pre row for diagnostics, and // kernel_dsv4_hc_split_weighted_sum, stores the HC-pre row for diagnostics, and
// reuses the just-collapsed values from threadgroup memory for the RMSNorm // reuses the just-collapsed values from threadgroup memory for the RMSNorm
// reduction. // reduction.
static __attribute__((always_inline)) inline void ds4_hc_comb_weights4_exact(
constant ds4_metal_args_dsv4_hc_split_weighted_sum_norm & args,
device volatile const float *mix,
device const float *scale,
device const float *base,
device float *out) {
const float epsv = args.eps;
const float comb_scale = scale[2];
float4 r0 =
*((device volatile const float4 *)(mix + 8)) * comb_scale +
*((device const float4 *)(base + 8));
float4 r1 =
*((device volatile const float4 *)(mix + 12)) * comb_scale +
*((device const float4 *)(base + 12));
float4 r2 =
*((device volatile const float4 *)(mix + 16)) * comb_scale +
*((device const float4 *)(base + 16));
float4 r3 =
*((device volatile const float4 *)(mix + 20)) * comb_scale +
*((device const float4 *)(base + 20));
const float m0 = max(max(r0.x, r0.y), max(r0.z, r0.w));
const float m1 = max(max(r1.x, r1.y), max(r1.z, r1.w));
const float m2 = max(max(r2.x, r2.y), max(r2.z, r2.w));
const float m3 = max(max(r3.x, r3.y), max(r3.z, r3.w));
r0 = exp(r0 - m0);
r1 = exp(r1 - m1);
r2 = exp(r2 - m2);
r3 = exp(r3 - m3);
r0 = r0 * (1.0f / (r0.x + r0.y + r0.z + r0.w)) + epsv;
r1 = r1 * (1.0f / (r1.x + r1.y + r1.z + r1.w)) + epsv;
r2 = r2 * (1.0f / (r2.x + r2.y + r2.z + r2.w)) + epsv;
r3 = r3 * (1.0f / (r3.x + r3.y + r3.z + r3.w)) + epsv;
float4 col_inv = 1.0f / (r0 + r1 + r2 + r3 + epsv);
r0 *= col_inv;
r1 *= col_inv;
r2 *= col_inv;
r3 *= col_inv;
for (int iter = 1; iter < args.sinkhorn_iters; ++iter) {
r0 *= 1.0f / (r0.x + r0.y + r0.z + r0.w + epsv);
r1 *= 1.0f / (r1.x + r1.y + r1.z + r1.w + epsv);
r2 *= 1.0f / (r2.x + r2.y + r2.z + r2.w + epsv);
r3 *= 1.0f / (r3.x + r3.y + r3.z + r3.w + epsv);
col_inv = 1.0f / (r0 + r1 + r2 + r3 + epsv);
r0 *= col_inv;
r1 *= col_inv;
r2 *= col_inv;
r3 *= col_inv;
}
*((device float4 *)(out + 8)) = r0;
*((device float4 *)(out + 12)) = r1;
*((device float4 *)(out + 16)) = r2;
*((device float4 *)(out + 20)) = r3;
}
kernel void kernel_dsv4_hc_split_weighted_sum_norm4( kernel void kernel_dsv4_hc_split_weighted_sum_norm4(
constant ds4_metal_args_dsv4_hc_split_weighted_sum_norm & args, constant ds4_metal_args_dsv4_hc_split_weighted_sum_norm & args,
device const char * mixes, device const char * mixes,
@@ -519,7 +567,6 @@ kernel void kernel_dsv4_hc_split_weighted_sum_norm4(
} }
threadgroup_barrier(mem_flags::mem_threadgroup); threadgroup_barrier(mem_flags::mem_threadgroup);
float sumf = 0.0f; float sumf = 0.0f;
for (uint i = tid; i < n4; i += ntg) { for (uint i = tid; i < n4; i += ntg) {
device const float4 *x0 = (device const float4 *)(x + 0 * args.nb_x1 + (uint64_t)row * args.nb_x2); device const float4 *x0 = (device const float4 *)(x + 0 * args.nb_x1 + (uint64_t)row * args.nb_x2);
@@ -884,6 +931,119 @@ kernel void kernel_dsv4_q8_hc_expand4_q8_0(
} }
} }
kernel void kernel_dsv4_q8_hc_expand4_q8_0_vec_hc(
constant ds4_metal_args_mul_mv & mv,
constant ds4_metal_args_dsv4_hc_expand & hc,
device const char * weight,
device const char * input,
device char * block_out,
device const char * residual,
device const char * post,
device const char * comb,
device char * dst,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
if (hc.n_hc != 4 || hc.n_tokens != 1) {
return;
}
const short NSG = FC_mul_mv_nsg;
constexpr short NW = N_SIMDWIDTH;
constexpr short NQ = 8;
constexpr short NR0 = N_R0_Q8_0;
const int nb = mv.ne00 / QK8_0;
const int row0 = tgpig.x * NR0;
const short ix = tiisg / (NW / NQ);
const short il = tiisg % (NW / NQ);
const int ib0 = sgitg * NQ + ix;
device const float *y = (device const float *)(input);
device const float *yb = y + ib0 * QK8_0 + il * NQ;
device const block_q8_0 *ax[NR0];
FOR_UNROLL(short row = 0; row < NR0; ++row) {
const uint64_t off0 = (uint64_t)(row0 + row) * mv.nb01;
ax[row] = (device const block_q8_0 *)(weight + off0);
}
float sumf[NR0] = { 0.0f };
float yl[NQ];
for (int ib = ib0; ib < nb; ib += NSG * NQ) {
FOR_UNROLL(short i = 0; i < NQ; ++i) {
yl[i] = yb[i];
}
FOR_UNROLL(short row = 0; row < NR0; ++row) {
device const int8_t *qs = ax[row][ib].qs + il * NQ;
float sumq = 0.0f;
FOR_UNROLL(short i = 0; i < NQ; ++i) {
sumq += qs[i] * yl[i];
}
sumf[row] += sumq * ax[row][ib].d;
}
yb += NSG * NQ * QK8_0;
}
threadgroup float *shmem_f32[NR0];
FOR_UNROLL(short row = 0; row < NR0; ++row) {
shmem_f32[row] = (threadgroup float *)shmem + NW * row;
if (sgitg == 0) {
shmem_f32[row][tiisg] = 0.0f;
}
sumf[row] = simd_sum(sumf[row]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL(short row = 0; row < NR0; ++row) {
if (tiisg == 0) {
shmem_f32[row][sgitg] = sumf[row];
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL(short row = 0; row < NR0; ++row) {
const int d = row0 + row;
if (d >= mv.ne01) {
continue;
}
const float block_v = simd_sum(shmem_f32[row][tiisg]);
if (tiisg == 0 && sgitg == 0) {
*((device float *)(block_out + (uint64_t)d * sizeof(float))) = block_v;
const float r0 = *((device const float *)(residual + (uint64_t)d * hc.nb_res0 + 0 * hc.nb_res1));
const float r1 = *((device const float *)(residual + (uint64_t)d * hc.nb_res0 + 1 * hc.nb_res1));
const float r2 = *((device const float *)(residual + (uint64_t)d * hc.nb_res0 + 2 * hc.nb_res1));
const float r3 = *((device const float *)(residual + (uint64_t)d * hc.nb_res0 + 3 * hc.nb_res1));
const float4 post4 = *((device const float4 *)post);
const float4 comb0 = *((device const float4 *)(comb + 0 * hc.nb_comb1));
const float4 comb1 = *((device const float4 *)(comb + 1 * hc.nb_comb1));
const float4 comb2 = *((device const float4 *)(comb + 2 * hc.nb_comb1));
const float4 comb3 = *((device const float4 *)(comb + 3 * hc.nb_comb1));
float4 acc = block_v * post4;
acc += comb0 * r0;
acc += comb1 * r1;
acc += comb2 * r2;
acc += comb3 * r3;
FOR_UNROLL (short dst_hc = 0; dst_hc < 4; ++dst_hc) {
*((device float *)(dst + (uint64_t)d * hc.nb0 +
(uint64_t)dst_hc * hc.nb1)) = acc[dst_hc];
}
}
}
}
// Reduces HC channels to a normal embedding row with the learned pre weights. // Reduces HC channels to a normal embedding row with the learned pre weights.
// This is the input adapter before the attention block and before the FFN block. // This is the input adapter before the attention block and before the FFN block.
kernel void kernel_dsv4_hc_weighted_sum( kernel void kernel_dsv4_hc_weighted_sum(
@@ -910,76 +1070,6 @@ kernel void kernel_dsv4_hc_weighted_sum(
*((device float *) (dst + d*args.nb0 + t*args.nb1)) = acc; *((device float *) (dst + d*args.nb0 + t*args.nb1)) = acc;
} }
// The one-row output head immediately applies a learned RMSNorm after reducing
// its four HC streams. Preserve the standalone scalar HC accumulation, write
// the collapsed row for diagnostics, then reload its F32 values from
// threadgroup memory using the standalone RMSNorm's float4 reduction mapping.
kernel void kernel_dsv4_hc_weighted_sum_norm4(
constant ds4_metal_args_dsv4_hc_weighted_sum_norm & args,
device const char * x,
device const char * weights,
device char * dst,
device const char * norm_weight,
device char * norm_dst,
threadgroup float * shared [[threadgroup(0)]],
ushort tid [[thread_position_in_threadgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort ntg [[threads_per_threadgroup]]) {
if (args.n_tokens != 1 || args.n_hc != 4 ||
args.n_embd <= 0 || (args.n_embd & 3) != 0) {
return;
}
const uint n_embd = uint(args.n_embd);
const uint n4 = n_embd >> 2;
threadgroup float *row_shmem = shared;
threadgroup float *sum_shmem = shared + n_embd;
if (sgitg == 0) {
sum_shmem[tiisg] = 0.0f;
}
for (uint d = tid; d < n_embd; d += ntg) {
float acc = 0.0f;
for (int64_t h = 0; h < args.n_hc; ++h) {
const float xv = *((device const float *)(
x + (uint64_t)d*args.nb_x0 + (uint64_t)h*args.nb_x1));
const float wv = *((device const float *)(
weights + (uint64_t)h*args.nb_w0));
acc += xv * wv;
}
row_shmem[d] = acc;
*((device float *)(dst + (uint64_t)d*args.nb0)) = acc;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
threadgroup const float4 *row4 =
(threadgroup const float4 *)row_shmem;
float sumf = 0.0f;
for (uint i = tid; i < n4; i += ntg) {
sumf += dot(row4[i], row4[i]);
}
sumf = simd_sum(sumf);
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tiisg == 0) {
sum_shmem[sgitg] = sumf;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
sumf = sum_shmem[tiisg];
sumf = simd_sum(sumf);
const float mean = sumf/args.n_embd;
const float scale = 1.0f/sqrt(mean + args.norm_eps);
device const float4 *w4 = (device const float4 *)norm_weight;
device float4 *norm4 = (device float4 *)norm_dst;
for (uint i = tid; i < n4; i += ntg) {
norm4[i] = (row4[i]*scale)*w4[i];
}
}
// The one-row HC=4 output head historically materializes four device-F32 // The one-row HC=4 output head historically materializes four device-F32
// stages across separate launches. Collapse those launches into one tiny // stages across separate launches. Collapse those launches into one tiny
// two-thread group while preserving the scalar/vector lane mapping and every // two-thread group while preserving the scalar/vector lane mapping and every
@@ -1015,3 +1105,440 @@ kernel void kernel_dsv4_output_hc_weights4(
args.post_scale * x + args.eps; args.post_scale * x + args.eps;
} }
} }
struct ds4_metal_args_hc_norm_mix {
int32_t n;
int32_t out_dim;
float eps;
};
// Fused unweighted RMSNorm + F16 HC-mix projection for DS4 decode HC-pre.
// The standalone decode path runs kernel_rms_norm_f32_4 over the flattened
// 4*embd HC row (1024 threads, one threadgroup) and then
// kernel_mul_mv_f16_f32_4 (nsg=8, nr0=2) over the normalized row. Both
// stages are reproduced bit-exactly in one dispatch: every threadgroup
// redundantly recomputes the norm partials with the original 1024-thread
// mapping (each real lane covers one virtual thread of each 256-thread
// slice, preserving every simd_sum tree), and the matvec keeps the original
// per-row accumulation order with y = x*scale computed on the fly, which
// rounds identically to the materialized normalized row. The host wrapper
// gates this to n == 16384 && out_dim == 24, where the virtual-thread count
// is exactly 1024 and the mv tail loop is empty.
kernel void kernel_dsv4_hc_rms_norm_mix_f16(
constant ds4_metal_args_hc_norm_mix & args,
device const char * x,
device const char * weight,
device char * dst,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
constexpr short NSG = 8; // ds4_gpu_make_plain_mv_dispatch(16384)
constexpr short NW = N_SIMDWIDTH;
constexpr short NR0 = 2; // plain mv nr0
constexpr short NB = 32;
constexpr short NF = 16;
constexpr short NF4 = NF/4;
constexpr uint VTHREADS = 1024u; // rms norm threads at n == 16384
constexpr short VSLICES = VTHREADS/(NSG*NW); // virtual 256-thread slices
const uint n = (uint)args.n;
const uint n4 = n >> 2;
device const float4 *x4 = (device const float4 *)x;
threadgroup float *norm_shmem = (threadgroup float *)shmem; // NW slots
threadgroup float *mv_shmem = (threadgroup float *)shmem + NW; // NW*NR0 slots
// Phase A: exact replica of kernel_rms_norm_f32_4's reduction tree with
// the 1024 virtual threads folded onto this threadgroup's 8 simdgroups.
for (short v = 0; v < VSLICES; ++v) {
const uint vt = (uint)(sgitg + NSG*v)*NW + tiisg;
float sumf = 0.0f;
for (uint i00 = vt; i00 < n4; i00 += VTHREADS) {
sumf += dot(x4[i00], x4[i00]);
}
sumf = simd_sum(sumf);
if (tiisg == 0) {
norm_shmem[sgitg + NSG*v] = sumf;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float total = norm_shmem[tiisg];
total = simd_sum(total);
const float mean = total/(float)args.n;
const float scale = 1.0f/sqrt(mean + args.eps);
// Phase B: exact replica of kernel_mul_mv_f16_f32_4 (nsg=8, nr0=2) with
// the normalized operand recomputed as x*scale instead of reloaded.
const int nb = args.n/NB;
const int r0 = tgpig.x*NR0;
device const half4 * ax4[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
ax4[row] = (device const half4 *)
(weight + (uint64_t)(r0 + row)*(uint64_t)n*sizeof(half));
}
float sumf_mv[NR0] = { 0.f };
const short ix = tiisg/(NW/NF);
const short il = tiisg%(NW/NF);
const int ib0 = sgitg*NF + ix;
for (int ib = ib0; ib < nb; ib += NSG*NF) {
float4 yl4[NF4];
FOR_UNROLL (short i = 0; i < NF4; ++i) {
yl4[i] = x4[(ib*NB + il*NF)/4 + i]*scale;
}
FOR_UNROLL (short row = 0; row < NR0; row++) {
device const half4 * xb4 = ax4[row] + (ib*NB + il*NF)/4;
float sumq = 0.f;
FOR_UNROLL (short i = 0; i < NF4; ++i) {
sumq += dot(float4(xb4[i]), yl4[i]);
}
sumf_mv[row] += sumq;
}
}
// n == 16384 makes the scalar tail loop of the original empty.
device float * dst_f32 = (device float *) dst;
helper_mv_reduce_and_write<NR0>(dst_f32, sumf_mv, r0, args.out_dim,
tiisg, sgitg, (threadgroup char *)mv_shmem);
}
// M5 specialization: pack two exact NR0=2 HC-mix producer groups into one
// 512-thread group. Two independent eight-simdgroup clusters retain the
// matvec reductions while the exact RMS scale is redundantly formed six,
// rather than twelve, times.
kernel void kernel_dsv4_hc_rms_norm_mix_f16_cluster2(
constant ds4_metal_args_hc_norm_mix & args,
device const char * x,
device const char * weight,
device char * dst,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
constexpr short NSG_CLUSTER = 8;
constexpr short NCLUSTER = 2;
constexpr short NSG_TOTAL = NSG_CLUSTER * NCLUSTER;
constexpr short NW = N_SIMDWIDTH;
constexpr short NR0 = 2;
constexpr short NB = 32;
constexpr short NF = 16;
constexpr short NF4 = NF/4;
constexpr uint VTHREADS = 1024u;
constexpr short VSLICES = VTHREADS/(NSG_TOTAL*NW);
const uint n = (uint)args.n;
const uint n4 = n >> 2;
device const float4 *x4 = (device const float4 *)x;
threadgroup float *norm_shmem = (threadgroup float *)shmem;
threadgroup float *mv_shmem = norm_shmem + NW;
// Exact 1024-virtual-thread RMS reduction, now folded two ways over
// the 16 physical simdgroups instead of four ways over eight.
for (short v = 0; v < VSLICES; ++v) {
const uint vt = (uint)(sgitg + NSG_TOTAL*v)*NW + tiisg;
float sumf = 0.0f;
for (uint i00 = vt; i00 < n4; i00 += VTHREADS) {
sumf += dot(x4[i00], x4[i00]);
}
sumf = simd_sum(sumf);
if (tiisg == 0) {
norm_shmem[sgitg + NSG_TOTAL*v] = sumf;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float total = norm_shmem[tiisg];
total = simd_sum(total);
const float mean = total/(float)args.n;
const float scale = 1.0f/sqrt(mean + args.eps);
// Two independent eight-simdgroup clusters reproduce two original
// NR0=2 matvec threadgroups inside this 512-thread threadgroup.
const short cluster = sgitg / NSG_CLUSTER;
const short local_sg = sgitg - cluster*NSG_CLUSTER;
const int nb = args.n/NB;
const int r0 = (int)tgpig.x*(NCLUSTER*NR0) + cluster*NR0;
device const half4 *ax4[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
ax4[row] = (device const half4 *)
(weight + (uint64_t)(r0 + row)*(uint64_t)n*sizeof(half));
}
float sumf_mv[NR0] = { 0.f };
const short ix = tiisg/(NW/NF);
const short il = tiisg%(NW/NF);
const int ib0 = local_sg*NF + ix;
for (int ib = ib0; ib < nb; ib += NSG_CLUSTER*NF) {
float4 yl4[NF4];
FOR_UNROLL (short i = 0; i < NF4; ++i) {
yl4[i] = x4[(ib*NB + il*NF)/4 + i]*scale;
}
FOR_UNROLL (short row = 0; row < NR0; ++row) {
device const half4 *xb4 = ax4[row] + (ib*NB + il*NF)/4;
float sumq = 0.f;
FOR_UNROLL (short i = 0; i < NF4; ++i) {
sumq += dot(float4(xb4[i]), yl4[i]);
}
sumf_mv[row] += sumq;
}
}
threadgroup float *cluster_shmem[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
cluster_shmem[row] = mv_shmem +
((uint)cluster*NR0 + row)*NW;
if (local_sg == 0) {
cluster_shmem[row][tiisg] = 0.0f;
}
sumf_mv[row] = simd_sum(sumf_mv[row]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL (short row = 0; row < NR0; ++row) {
if (tiisg == 0) {
cluster_shmem[row][local_sg] = sumf_mv[row];
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
device float *mixes_f32 = (device float *)dst;
FOR_UNROLL (short row = 0; row < NR0; ++row) {
const float tot = simd_sum(cluster_shmem[row][tiisg]);
if (tiisg == 0 && local_sg == 0 && r0 + row < args.out_dim) {
mixes_f32[r0 + row] = tot;
}
}
}
kernel void kernel_dsv4_hc_rms_norm_mix_f16_cluster2_pre_norm(
constant ds4_metal_args_hc_norm_mix & args,
constant ds4_metal_args_dsv4_hc_split_weighted_sum_norm & split_args,
device const char * x,
device const char * weight,
device char * dst,
device const float * hc_scale,
device const float * hc_base,
device char * split,
device char * collapse_dst,
device const char * norm_weight,
device char * norm_dst,
device atomic_uint * completion,
threadgroup char * shmem [[threadgroup(0)]],
uint3 tgpig [[threadgroup_position_in_grid]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
constexpr short NSG_CLUSTER = 8;
constexpr short NCLUSTER = 2;
constexpr short NSG_TOTAL = NSG_CLUSTER * NCLUSTER;
constexpr short NW = N_SIMDWIDTH;
constexpr short NR0 = 2;
constexpr short NB = 32;
constexpr short NF = 16;
constexpr short NF4 = NF/4;
constexpr uint VTHREADS = 1024u;
constexpr short VSLICES = VTHREADS/(NSG_TOTAL*NW);
const uint n = (uint)args.n;
const uint n4 = n >> 2;
device const float4 *x4 = (device const float4 *)x;
threadgroup float *norm_shmem = (threadgroup float *)shmem;
threadgroup float *mv_shmem = norm_shmem + NW;
// Exact 1024-virtual-thread RMS reduction, now folded two ways over
// the 16 physical simdgroups instead of four ways over eight.
for (short v = 0; v < VSLICES; ++v) {
const uint vt = (uint)(sgitg + NSG_TOTAL*v)*NW + tiisg;
float sumf = 0.0f;
for (uint i00 = vt; i00 < n4; i00 += VTHREADS) {
sumf += dot(x4[i00], x4[i00]);
}
sumf = simd_sum(sumf);
if (tiisg == 0) {
norm_shmem[sgitg + NSG_TOTAL*v] = sumf;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float total = norm_shmem[tiisg];
total = simd_sum(total);
const float mean = total/(float)args.n;
const float scale = 1.0f/sqrt(mean + args.eps);
// Two independent eight-simdgroup clusters reproduce two original
// NR0=2 matvec threadgroups inside this 512-thread threadgroup.
const short cluster = sgitg / NSG_CLUSTER;
const short local_sg = sgitg - cluster*NSG_CLUSTER;
const int nb = args.n/NB;
const int r0 = (int)tgpig.x*(NCLUSTER*NR0) + cluster*NR0;
device const half4 *ax4[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
ax4[row] = (device const half4 *)
(weight + (uint64_t)(r0 + row)*(uint64_t)n*sizeof(half));
}
float sumf_mv[NR0] = { 0.f };
const short ix = tiisg/(NW/NF);
const short il = tiisg%(NW/NF);
const int ib0 = local_sg*NF + ix;
for (int ib = ib0; ib < nb; ib += NSG_CLUSTER*NF) {
float4 yl4[NF4];
FOR_UNROLL (short i = 0; i < NF4; ++i) {
yl4[i] = x4[(ib*NB + il*NF)/4 + i]*scale;
}
FOR_UNROLL (short row = 0; row < NR0; ++row) {
device const half4 *xb4 = ax4[row] + (ib*NB + il*NF)/4;
float sumq = 0.f;
FOR_UNROLL (short i = 0; i < NF4; ++i) {
sumq += dot(float4(xb4[i]), yl4[i]);
}
sumf_mv[row] += sumq;
}
}
threadgroup float *cluster_shmem[NR0];
FOR_UNROLL (short row = 0; row < NR0; ++row) {
cluster_shmem[row] = mv_shmem +
((uint)cluster*NR0 + row)*NW;
if (local_sg == 0) {
cluster_shmem[row][tiisg] = 0.0f;
}
sumf_mv[row] = simd_sum(sumf_mv[row]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
FOR_UNROLL (short row = 0; row < NR0; ++row) {
if (tiisg == 0) {
cluster_shmem[row][local_sg] = sumf_mv[row];
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
device volatile float *mixes_f32 =
(device volatile float *)dst;
if (local_sg == 0) {
FOR_UNROLL (short row = 0; row < NR0; ++row) {
const float tot = simd_sum(cluster_shmem[row][tiisg]);
if (tiisg == 0 && r0 + row < args.out_dim) {
mixes_f32[r0 + row] = tot;
}
}
}
// The first producer group owns mix[0:4]. After materializing and
// reloading those values, fold the established 1024-thread HC collapse
// and RMS reduction over this group's 512 physical threads as two
// independent virtual slices. This retains the original 32-partial tree.
threadgroup_barrier(mem_flags::mem_device_and_threadgroup);
const uint tid = (uint)sgitg * (uint)NW + (uint)tiisg;
threadgroup float *pre_shmem = norm_shmem + 32u + 4u*NW;
threadgroup float *sum_shmem = pre_shmem + 4;
if (tgpig.x == 0) {
device float *out = (device float *)split;
if (tid == 0) {
const float4 pre_z =
*((device volatile const float4 *)mixes_f32) * hc_scale[0] +
*((device const float4 *)hc_base);
const float4 pre =
1.0f / (1.0f + exp(-pre_z)) + split_args.eps;
*((device float4 *)out) = pre;
pre_shmem[0] = pre.x;
pre_shmem[1] = pre.y;
pre_shmem[2] = pre.z;
pre_shmem[3] = pre.w;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const uint n4_collapse = uint(split_args.n_embd) >> 2;
const uint i0 = tid;
const uint i1 = tid + 512u;
device const float4 *x0 = (device const float4 *)(
x + 0 * split_args.nb_x1);
device const float4 *x1 = (device const float4 *)(
x + 1 * split_args.nb_x1);
device const float4 *x2 = (device const float4 *)(
x + 2 * split_args.nb_x1);
device const float4 *x3 = (device const float4 *)(
x + 3 * split_args.nb_x1);
float4 v0 = 0.0f;
v0 += x0[i0] * pre_shmem[0];
v0 += x1[i0] * pre_shmem[1];
v0 += x2[i0] * pre_shmem[2];
v0 += x3[i0] * pre_shmem[3];
float sum0 = simd_sum(dot(v0, v0));
float4 v1 = 0.0f;
if (i1 < n4_collapse) {
v1 += x0[i1] * pre_shmem[0];
v1 += x1[i1] * pre_shmem[1];
v1 += x2[i1] * pre_shmem[2];
v1 += x3[i1] * pre_shmem[3];
}
float sum1 = simd_sum(dot(v1, v1));
if (tiisg == 0) {
sum_shmem[sgitg] = sum0;
sum_shmem[sgitg + 16] = sum1;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float sumf = sum_shmem[tiisg];
sumf = simd_sum(sumf);
const float norm_arg =
sumf / float(split_args.n_embd) + split_args.norm_eps;
const float norm_scale = rsqrt(norm_arg);
device float4 *dst4 = (device float4 *)collapse_dst;
device const float4 *w4 = (device const float4 *)norm_weight;
device float4 *norm4 = (device float4 *)norm_dst;
dst4[i0] = v0;
norm4[i0] = (v0 * norm_scale) * w4[i0];
if (i1 < n4_collapse) {
dst4[i1] = v1;
norm4[i1] = (v1 * norm_scale) * w4[i1];
}
} else if (tgpig.x == 1 && tid == 0) {
device float *out = (device float *)split;
const float4 post_z =
*((device volatile const float4 *)(mixes_f32 + 4)) * hc_scale[1] +
*((device const float4 *)(hc_base + 4));
*((device float4 *)(out + 4)) = 2.0f / (1.0f + exp(-post_z));
}
// Groups 2..5 own exactly the comb range consumed by the
// continuation. Their four-way completion overlaps TG0's independent
// pre-collapse/RMS epilogue. Every writer crosses the uniform publish
// fence; only lane zero then participates in the completion protocol.
atomic_thread_fence(mem_flags::mem_device,
memory_order_seq_cst,
thread_scope_device);
if (tgpig.x < 2 || tid != 0) {
return;
}
const uint old = atomic_fetch_add_explicit(
completion, 1u, memory_order_relaxed);
if (old + 1u != 4u) {
return;
}
atomic_thread_fence(mem_flags::mem_device,
memory_order_seq_cst,
thread_scope_device);
ds4_hc_comb_weights4_exact(
split_args, mixes_f32, hc_scale, hc_base,
(device float *)split);
atomic_thread_fence(mem_flags::mem_device,
memory_order_seq_cst,
thread_scope_device);
atomic_store_explicit(completion, 0u, memory_order_relaxed);
}
+222
View File
@@ -319,6 +319,228 @@ kernel void kernel_dsv4_compressor_pack_ratio4(
} }
} }
// Decode already holds the complete ratio-4 recurrent window in state layout:
// eight rows of two head_dim planes. Pack the previous plane from rows 0..3
// and the current plane from rows 4..7 directly into the transposed [head_dim,
// 8] layout consumed by the exact GGML softmax/multiply/sum sequence. KV and
// score move together; no arithmetic or reduction order changes.
kernel void kernel_dsv4_compressor_pack_ratio4_decode_ggml(
constant ds4_metal_args_dsv4_compressor_pack_ratio4 & args,
device const uint * state_kv,
device const uint * state_score,
device uint * packed_kv,
device uint * packed_score,
uint row [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]]) {
if (row >= 8u || args.head_dim == 0u || args.n_threads == 0u) {
return;
}
const uint64_t state_row_stride = 2ull * args.head_dim;
const uint64_t src_plane = row >= 4u ? args.head_dim : 0u;
for (uint col = tid; col < args.head_dim; col += args.n_threads) {
const uint64_t src = (uint64_t)row * state_row_stride +
src_plane + col;
const uint64_t dst = (uint64_t)col * 8u + row;
packed_kv[dst] = state_kv[src];
packed_score[dst] = state_score[src];
}
}
// Exact decode specialization for the first two operations in GGML's
// softmax -> multiply -> sum_rows compressor reduction. The normalized
// softmax values are deliberately materialized in device memory and reloaded
// after a device barrier before the in-place product, preserving the dispatch
// boundary's float store/load semantics. The final sum remains the standalone
// eight-thread sum_rows kernel: changing a 32-thread group into the original
// eight-thread reduction inside this kernel would make its threadgroup
// barriers non-uniform or alter simd_sum's active-lane topology.
kernel void kernel_dsv4_compressor_exact_softmax_product_ratio4(
constant ds4_metal_args_dsv4_compressor_pack_ratio4 & args,
device const float * packed_kv,
device const float * packed_score,
device float * softmax,
device float * product,
threadgroup float * softmax_scratch [[threadgroup(0)]],
uint row [[threadgroup_position_in_grid]],
uint tid [[thread_position_in_threadgroup]]) {
if (row >= args.head_dim || args.n_comp != 1u ||
args.n_threads != 32u) {
return;
}
device const float4 * score4 =
(device const float4 *)(packed_score + (uint64_t)row * 8u);
device float4 * softmax4 =
(device float4 *)(softmax + (uint64_t)row * 8u);
const float scale = (float)args.replay;
const float zero = (float)(args.n_comp - 1u);
// Match kernel_soft_max_f32_4(width=8, nth=32) literally. Only lanes zero
// and one own float4s, while all 32 lanes participate in both reductions.
float4 lmax4 = -INFINITY;
for (int i00 = (int)tid; i00 < 2; i00 += 32) {
lmax4 = fmax(lmax4, score4[i00] * scale + (float4)zero);
}
const float lmax =
MAX(MAX(lmax4[0], lmax4[1]), MAX(lmax4[2], lmax4[3]));
const float max_val = simd_max(lmax);
float4 lsum4 = 0.0f;
for (int i00 = (int)tid; i00 < 2; i00 += 32) {
const float4 exp_score4 =
exp((score4[i00] * scale + (float4)zero) - max_val);
lsum4 += exp_score4;
softmax4[i00] = exp_score4;
}
const float lsum =
lsum4[0] + lsum4[1] + lsum4[2] + lsum4[3];
threadgroup_barrier(mem_flags::mem_none);
const float sum = simd_sum(lsum);
const float inv_sum = 1.0f / sum;
for (int i00 = (int)tid; i00 < 2; i00 += 32) {
softmax4[i00] *= inv_sum;
}
// Force the same normalized-softmax device store/reload boundary that the
// separate multiply dispatch observes.
threadgroup_barrier(mem_flags::mem_device);
device volatile const float * reloaded_softmax =
(device volatile const float *)(softmax + (uint64_t)row * 8u);
device const float * kv_row = packed_kv + (uint64_t)row * 8u;
device float * product_row = product + (uint64_t)row * 8u;
// Match kernel_bin_fuse_f32_f32_f32(width=8, nth=4): four lanes each
// process their low element followed by the element four positions later.
if (tid < 4u) {
for (uint i0 = tid; i0 < 8u; i0 += 4u) {
float value = kv_row[i0];
value *= reloaded_softmax[i0];
product_row[i0] = value;
}
}
// All 32 lanes reach the final device barrier. The following standalone
// sum_rows dispatch performs the required global reload and exact TG8
// two-stage simd_sum topology.
threadgroup_barrier(mem_flags::mem_device);
(void)softmax_scratch;
}
// Exact one-dispatch ratio-4 decode pool. This specializes the three-dispatch
// pack -> exact softmax/product -> sum_rows chain above without changing any
// floating-point operation or reduction topology. The normalized softmax and
// product are still materialized and volatile-reloaded through device memory.
// The two simd_sum calls in the final reduction execute under an eight-lane
// active mask, exactly matching kernel_sum_rows_f32_f32's original TG8.
kernel void kernel_dsv4_compressor_exact_pool_ratio4_decode_ggml(
constant ds4_metal_args_dsv4_compressor_pack_ratio4 & args,
device const float * state_kv,
device const float * state_score,
device float * softmax,
device float * product,
device float * dst,
threadgroup float * sum_scratch [[threadgroup(0)]],
uint col [[threadgroup_position_in_grid]],
uint tid [[thread_position_in_threadgroup]]) {
if (col >= args.head_dim || args.n_comp != 1u ||
args.n_threads != 32u) {
return;
}
const uint64_t state_row_stride = 2ull * args.head_dim;
const float scale = (float)args.replay;
const float zero = (float)(args.n_comp - 1u);
// Match the packed float4 ownership: lane 0 owns rows 0..3 and lane 1
// rows 4..7. The gather itself is an integer-addressed bit-preserving load.
float4 score_values = -INFINITY;
if (tid < 2u) {
const uint row0 = 4u * tid;
for (uint j = 0u; j < 4u; ++j) {
const uint row = row0 + j;
const uint64_t src = (uint64_t)row * state_row_stride +
(row >= 4u ? args.head_dim : 0u) + col;
score_values[j] = state_score[src];
}
}
const uint64_t scratch_base = (uint64_t)col * 8u;
device float4 * softmax4 =
(device float4 *)(softmax + scratch_base);
// Verbatim kernel_soft_max_f32_4(width=8, nth=32) arithmetic.
float4 lmax4 = -INFINITY;
for (int i00 = (int)tid; i00 < 2; i00 += 32) {
lmax4 = fmax(lmax4, score_values * scale + (float4)zero);
}
const float lmax =
MAX(MAX(lmax4[0], lmax4[1]), MAX(lmax4[2], lmax4[3]));
const float max_val = simd_max(lmax);
float4 lsum4 = 0.0f;
for (int i00 = (int)tid; i00 < 2; i00 += 32) {
const float4 exp_score4 =
exp((score_values * scale + (float4)zero) - max_val);
lsum4 += exp_score4;
softmax4[i00] = exp_score4;
}
const float lsum =
lsum4[0] + lsum4[1] + lsum4[2] + lsum4[3];
threadgroup_barrier(mem_flags::mem_none);
const float sum = simd_sum(lsum);
const float inv_sum = 1.0f / sum;
for (int i00 = (int)tid; i00 < 2; i00 += 32) {
softmax4[i00] *= inv_sum;
}
threadgroup_barrier(mem_flags::mem_device);
device volatile const float * reloaded_softmax =
(device volatile const float *)(softmax + scratch_base);
device float * product_row = product + scratch_base;
// Verbatim width=8, TG4 multiply ownership: low element, then +4.
if (tid < 4u) {
for (uint i0 = tid; i0 < 8u; i0 += 4u) {
const uint64_t src = (uint64_t)i0 * state_row_stride +
(i0 >= 4u ? args.head_dim : 0u) + col;
float value = state_kv[src];
value *= reloaded_softmax[i0];
product_row[i0] = value;
}
}
// Preserve the product dispatch's device store/reload boundary.
threadgroup_barrier(mem_flags::mem_device);
device volatile const float * reloaded_product =
(device volatile const float *)product_row;
// Reproduce kernel_sum_rows_f32_f32(width=8, TG8) literally. MSL defines
// simdgroup collectives over active lanes, so the branch recreates the
// original eight-lane partial SIMD group inside this 32-thread group.
sum_scratch[tid] = 0.0f;
float row_sum = 0.0f;
if (tid < 8u) {
row_sum += reloaded_product[tid];
row_sum = simd_sum(row_sum);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tid == 0u) {
sum_scratch[0] = row_sum;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tid < 8u) {
row_sum = sum_scratch[tid];
row_sum = simd_sum(row_sum);
if (tid == 0u) {
dst[col] = row_sum;
}
}
}
// Ratio-4 compression keeps two 4-row halves of recurrent state. After an // Ratio-4 compression keeps two 4-row halves of recurrent state. After an
// emitted compressed row, the second half becomes the next window's previous // emitted compressed row, the second half becomes the next window's previous
// half. The old encoder expressed this as four generic copies; this DS4-specific // half. The old encoder expressed this as four generic copies; this DS4-specific
+787 -48
View File
File diff suppressed because it is too large Load Diff
+518 -20
View File
@@ -41,6 +41,23 @@ struct ds4_metal_args_dsv4_rope_affine_pair {
float beta_slow; float beta_slow;
}; };
struct ds4_metal_args_dsv4_head_norm_rope {
int32_t n_head;
int32_t head_dim;
int32_t head_dim4;
int32_t n_dims;
int32_t n_ctx_orig;
int32_t pos0;
int32_t inverse;
float eps;
float freq_base;
float freq_scale;
float ext_factor;
float attn_factor;
float beta_fast;
float beta_slow;
};
static float rope_yarn_ramp(const float low, const float high, const int i0) { static float rope_yarn_ramp(const float low, const float high, const int i0) {
const float y = (i0 / 2 - low) / max(0.001f, high - low); const float y = (i0 / 2 - low) / max(0.001f, high - low);
return 1.0f - min(1.0f, max(0.0f, y)); return 1.0f - min(1.0f, max(0.0f, y));
@@ -327,36 +344,109 @@ kernel void kernel_dsv4_rope_tail_f32_inplace_pair_shared4(
*((device float *) (dst_base + j1*args.nb0)) = x0*sin_theta + x1*cos_theta; *((device float *) (dst_base + j1*args.nb0)) = x0*sin_theta + x1*cos_theta;
} }
// DS4 positions are always affine within one RoPE dispatch. This variant // Fuses the per-head RMSNorm and partial Q RoPE while retaining the standalone
// reconstructs the same wrapped int32 position in-kernel, avoiding the host // norm reduction tree and the mode-0 RoPE lane mapping.
// position array and its buffer binding while preserving the pair lane mapping kernel void kernel_dsv4_head_rms_norm_rope_tail_f32(
// and all floating-point operations of the specialization above. constant ds4_metal_args_dsv4_head_norm_rope & args,
kernel void kernel_dsv4_rope_tail_f32_inplace_pair_affine( device char * xraw,
constant ds4_metal_args_dsv4_rope_affine_pair & args [[buffer(0)]], threadgroup float * shmem_f32 [[threadgroup(0)]],
device const char * src0 [[buffer(1)]], uint3 tgpig [[threadgroup_position_in_grid]],
device char * dst [[buffer(4)]], ushort3 tpitg [[thread_position_in_threadgroup]],
uint tid [[thread_index_in_threadgroup]], ushort sgitg [[simdgroup_index_in_threadgroup]],
ushort3 ntg [[threads_per_threadgroup]], ushort tiisg [[thread_index_in_simdgroup]],
uint3 tgpig [[threadgroup_position_in_grid]]) { ushort3 ntg [[threads_per_threadgroup]]) {
const int i1 = tgpig[0]; if (sgitg == 0) {
const int i2 = tgpig[1]; shmem_f32[tiisg] = 0.0f;
}
const uint head = tgpig.x;
const uint tok = tgpig.y;
device float4 * x4 = (device float4 *)xraw +
((uint64_t)tok * (uint64_t)args.n_head + head) *
(uint64_t)args.head_dim4;
float sumf = 0.0f;
for (int i00 = tpitg.x; i00 < args.head_dim4; i00 += ntg.x) {
sumf += dot(x4[i00], x4[i00]);
}
sumf = simd_sum(sumf);
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tiisg == 0) {
shmem_f32[sgitg] = sumf;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
sumf = simd_sum(shmem_f32[tiisg]);
const float scale = 1.0f / sqrt(sumf / args.head_dim + args.eps);
const int n_nope = args.head_dim - args.n_dims; const int n_nope = args.head_dim - args.n_dims;
if (n_nope < 0) { if (n_nope < 0) {
return; return;
} }
float corr_dims[2];
rope_yarn_corr_dims(args.n_dims, args.n_ctx_orig, args.freq_base,
args.beta_fast, args.beta_slow, corr_dims);
const float theta_base = (float)(args.pos0 + (int)tok);
const float inv_ndims = -1.0f / args.n_dims;
device float * xs = (device float *)x4;
for (int i0 = tpitg.x; i0 < args.head_dim; i0 += ntg.x) {
if (i0 < n_nope) {
xs[i0] = xs[i0] * scale;
continue;
}
const int r = i0 - n_nope;
if ((r & 1) != 0) {
continue;
}
#ifdef DS4_METAL_ROPE_EXP2_LOG2
const float theta =
theta_base * exp2(inv_ndims * (float)r * log2(args.freq_base));
#else
const float theta =
theta_base * pow(args.freq_base, inv_ndims * r);
#endif
float cos_theta;
float sin_theta;
rope_yarn(theta, args.freq_scale, corr_dims, r,
args.ext_factor, args.attn_factor,
&cos_theta, &sin_theta);
if (args.inverse) {
sin_theta = -sin_theta;
}
const float x0 = xs[i0] * scale;
const float x1 = xs[i0 + 1] * scale;
xs[i0] = x0 * cos_theta - x1 * sin_theta;
xs[i0 + 1] = x0 * sin_theta + x1 * cos_theta;
}
}
// DS4 positions are always affine within one RoPE dispatch. This variant
// reconstructs the same wrapped int32 position in-kernel, avoiding the host
// position array and its buffer binding while preserving the pair lane mapping
// and all floating-point operations of the specialization above.
/* Shared, deliberately noinline so that every caller gets bit-identical
* trigonometric codegen. The header note about tiny trig codegen changes
* flipping sampled tokens is exactly why this body must be compiled once and
* shared rather than inlined separately into each kernel. */
static __attribute__((noinline)) void ds4_rope_tail_pair_affine_row(
constant ds4_metal_args_dsv4_rope_affine_pair & args,
device const char * src_base,
device char * dst_base,
int n_nope,
uint raw_pos,
uint tid,
uint nthreads) {
float corr_dims[2]; float corr_dims[2];
rope_yarn_corr_dims(args.n_dims, args.n_ctx_orig, args.freq_base, args.beta_fast, args.beta_slow, corr_dims); rope_yarn_corr_dims(args.n_dims, args.n_ctx_orig, args.freq_base, args.beta_fast, args.beta_slow, corr_dims);
const uint raw_pos = args.pos0 + (uint)i2 * args.pos_step; const float theta_base = (float)as_type<int>(raw_pos);
const float theta_base = (float)as_type<int>(raw_pos);
const float inv_ndims = -1.f/args.n_dims; const float inv_ndims = -1.f/args.n_dims;
device const char * src_base =
src0 + (uint64_t)i2*args.token_bytes + (uint64_t)i1*args.row_bytes;
device char * dst_base =
dst + (uint64_t)i2*args.token_bytes + (uint64_t)i1*args.row_bytes;
for (int r = tid; r < args.n_dims; r += ntg.x) { for (int r = tid; r < args.n_dims; r += nthreads) {
if ((r & 1) != 0) { if ((r & 1) != 0) {
continue; continue;
} }
@@ -381,5 +471,413 @@ kernel void kernel_dsv4_rope_tail_f32_inplace_pair_affine(
*((device float *) (dst_base + j0*sizeof(float))) = x0*cos_theta - x1*sin_theta; *((device float *) (dst_base + j0*sizeof(float))) = x0*cos_theta - x1*sin_theta;
*((device float *) (dst_base + j1*sizeof(float))) = x0*sin_theta + x1*cos_theta; *((device float *) (dst_base + j1*sizeof(float))) = x0*sin_theta + x1*cos_theta;
}}
kernel void kernel_dsv4_rope_tail_f32_inplace_pair_affine(
constant ds4_metal_args_dsv4_rope_affine_pair & args [[buffer(0)]],
device const char * src0 [[buffer(1)]],
device char * dst [[buffer(4)]],
uint tid [[thread_index_in_threadgroup]],
ushort3 ntg [[threads_per_threadgroup]],
uint3 tgpig [[threadgroup_position_in_grid]]) {
const int i1 = tgpig[0];
const int i2 = tgpig[1];
const int n_nope = args.head_dim - args.n_dims;
if (n_nope < 0) {
return;
}
const uint raw_pos = args.pos0 + (uint)i2 * args.pos_step;
device const char * src_base =
src0 + (uint64_t)i2*args.token_bytes + (uint64_t)i1*args.row_bytes;
device char * dst_base =
dst + (uint64_t)i2*args.token_bytes + (uint64_t)i1*args.row_bytes;
ds4_rope_tail_pair_affine_row(args, src_base, dst_base, n_nope, raw_pos, tid, ntg.x);
}
// Decode-only fusion of the KV RoPE tail with the FP8/raw finalizer. Both were
// already single 64-thread threadgroups on the same row, back to back, so the
// pair cost two dispatches (~12.4 us) to touch 2 KB. The RoPE body below is a
// verbatim copy of kernel_dsv4_rope_tail_f32_inplace_pair_affine specialised to
// the decode grid (one head, one token, so i1 = i2 = 0) and the finalizer body
// is a verbatim copy of kernel_dsv4_kv_fp8_store_f32. The barrier between them
// is required because RoPE writes element pairs across lanes while the raw copy
// reads them per lane. Arithmetic, order and rounding are unchanged; the header
// warning above about trigonometric codegen still applies, so this kernel is
// gated and verified against full-vocabulary logits before promotion.
kernel void kernel_dsv4_kv_rope_fp8_store_f32(
constant ds4_metal_args_dsv4_kv_fp8_store & args,
constant ds4_metal_args_dsv4_rope_affine_pair & rope,
device float * kv,
device float * raw_cache,
threadgroup float * scratch [[threadgroup(0)]],
uint tid [[thread_index_in_threadgroup]]) {
{
const int rope_n_nope = rope.head_dim - rope.n_dims;
if (rope_n_nope < 0) {
return;
}
ds4_rope_tail_pair_affine_row(rope,
(device const char *)kv,
(device char *)kv,
rope_n_nope,
rope.pos0,
tid,
64u);
}
/* The RoPE helper writes device-memory pairs that different lanes read
* below. A threadgroup-only fence does not make those cross-lane device
* writes visible. */
threadgroup_barrier(mem_flags::mem_device_and_threadgroup);
{
const int head_dim = args.head_dim;
const int n_rot = args.n_rot;
const int n_nope = head_dim - n_rot;
if (head_dim <= 0 || n_rot < 0 || n_nope < 0 || tid >= 64) {
return;
}
device float * raw = raw_cache + (int64_t)args.raw_row * head_dim;
for (int off = 0; off < n_nope; off += 64) {
float v = 0.0f;
if (off + (int)tid < n_nope) {
v = kv[off + tid];
scratch[tid] = abs(v);
} else {
scratch[tid] = 0.0f;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (uint stride = 32; stride > 0; stride >>= 1) {
if (tid < stride) {
scratch[tid] = max(scratch[tid], scratch[tid + stride]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
const float amax = max(scratch[0], 1.0e-4f);
const float fp8_scale = exp2(ceil(log2(amax / 448.0f)));
if (off + (int)tid < n_nope) {
const float q = dsv4_e4m3fn_dequant(clamp(v / fp8_scale, -448.0f, 448.0f)) * fp8_scale;
kv[off + tid] = q;
// Diagnostic only: skip the FP16 round-trip that normally matches the
// half-typed FlashAttention KV buffer's precision. With this enabled the
// indexer will see higher-precision raw values than FlashAttention does,
// which is informative but not a production-ready setting.
#ifdef DS4_METAL_KV_RAW_F32
raw[off + tid] = q;
#else
raw[off + tid] = (float)((half)q);
#endif
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
for (int i = n_nope + tid; i < head_dim; i += 64) {
#ifdef DS4_METAL_KV_RAW_F32
raw[i] = kv[i];
#else
raw[i] = (float)((half)kv[i]);
#endif
}
} }
} }
/* Decode-only sibling of kernel_flash_attn_ext_vec_reduce that also applies the
* inverse RoPE tail to the row it just produced, removing a whole dispatch per
* layer. Each threadgroup owns one head's entire 512-float row, so the RoPE is
* an intra-threadgroup dependency: reduce, barrier, rotate. Both halves call the
* same shared noinline helpers the standalone kernels use, so the arithmetic and
* its codegen are identical to running the two dispatches back to back. */
kernel void kernel_flash_attn_ext_vec_reduce_rope(
constant ds4_metal_args_flash_attn_ext_vec_reduce & args,
device const char * htmp,
device char * dst,
constant ds4_metal_args_dsv4_rope_affine_pair & rope,
uint tgpig[[threadgroup_position_in_grid]],
ushort tiitg[[thread_index_in_threadgroup]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
ds4_flash_attn_vec_reduce_row(args, htmp, dst, tgpig, tiisg, sgitg,
(short)FC_flash_attn_ext_vec_reduce_NWG,
(short)FC_flash_attn_ext_vec_reduce_DV);
threadgroup_barrier(mem_flags::mem_device);
const int n_nope = rope.head_dim - rope.n_dims;
if (n_nope < 0) {
return;
}
device char * row = dst + (uint64_t)tgpig * rope.row_bytes;
ds4_rope_tail_pair_affine_row(rope,
(device const char *)row,
row,
n_nope,
rope.pos0,
tiitg,
(uint)(32 * FC_flash_attn_ext_vec_reduce_NWG));
}
struct ds4_metal_args_dsv4_comp_finalize {
ds4_metal_args_dsv4_rope_affine_pair rope;
float rms_eps;
uint32_t pad0;
};
/* Decode-only emit-path fusion. Every ratio-th token, each layer finalizes
* one freshly pooled compressor row per compressor: RMS norm, RoPE tail, and
* then the FP8 round-trip + F16 commit copy (attention, 512 floats) or the
* Hadamard+FP4 QAT (indexer, 128 floats). Those were seven single-row
* dispatches; this kernel is one dispatch with two threadgroups.
*
* Each phase reproduces its standalone kernel bit-exactly:
* - norm: kernel_rms_norm_mul_f32_4's tree (float4 lanes, simd_sum, zero-
* padded 32-slot cross-simdgroup reduce); 512 uses 128 virtual threads on
* simdgroups 0-3, 128 uses 32 virtual threads on simdgroup 0.
* - rope: ds4_rope_tail_pair_affine_row verbatim (lanes 0-63, nthreads=64).
* - fp8: kernel_dsv4_fp8_kv_quantize_f32's 64-lane shmem max tree and
* round-trip, src==dst so the verbatim tail copy is a no-op and dropped.
* - commit: per-element f32->f16 conversion (value-wise exact).
* - qat: kernel_dsv4_indexer_hadamard_fp4_f32's butterfly and per-32 amax
* tree on lanes 0-127.
* Threads outside a phase's virtual width still execute every barrier, so
* threadgroup barriers stay uniform across the 256-thread threadgroup. */
kernel void kernel_dsv4_comp_row_finalize_f32(
constant ds4_metal_args_dsv4_comp_finalize & args [[buffer(0)]],
device float * attn_row [[buffer(1)]],
device const float * attn_norm_w [[buffer(2)]],
device char * attn_cache [[buffer(3)]],
device float * index_row [[buffer(4)]],
device const float * index_norm_w [[buffer(5)]],
device float * attn_state_kv [[buffer(6)]],
device float * attn_state_score [[buffer(7)]],
device float * index_state_kv [[buffer(8)]],
device float * index_state_score [[buffer(9)]],
threadgroup float * shmem [[threadgroup(0)]],
uint tgpig [[threadgroup_position_in_grid]],
ushort tiitg [[thread_index_in_threadgroup]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
constant ds4_metal_args_dsv4_rope_affine_pair & rope_args = args.rope;
if (tgpig == 0) {
/* -------- attention compressor row (512 floats) -------- */
{
device float4 * y4 = (device float4 *)attn_row;
device const float4 * x4 = (device const float4 *)attn_row;
device const float4 * w4 = (device const float4 *)attn_norm_w;
if (sgitg == 0) {
shmem[tiisg] = 0.0f;
}
float sumf = 0.0f;
if (tiitg < 128) {
sumf = dot(x4[tiitg], x4[tiitg]);
}
sumf = simd_sum(sumf);
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tiitg < 128 && tiisg == 0) {
shmem[sgitg] = sumf;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float total = 0.0f;
if (tiitg < 128) {
total = simd_sum(shmem[tiisg]);
}
const float mean = total / 512.0f;
const float scale = 1.0f/sqrt(mean + args.rms_eps);
if (tiitg < 128) {
y4[tiitg] = (x4[tiitg]*scale)*w4[tiitg];
}
}
threadgroup_barrier(mem_flags::mem_device);
ds4_rope_tail_pair_affine_row(rope_args,
(device const char *)attn_row,
(device char *)attn_row,
512 - rope_args.n_dims,
rope_args.pos0,
tiitg,
64u);
threadgroup_barrier(mem_flags::mem_device);
for (int off = 0; off < 512 - rope_args.n_dims; off += 64) {
float v = 0.0f;
if (tiitg < 64) {
v = attn_row[off + tiitg];
shmem[tiitg] = abs(v);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (uint stride = 32; stride > 0; stride >>= 1) {
if (tiitg < stride) {
shmem[tiitg] = max(shmem[tiitg], shmem[tiitg + stride]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
const float amax = max(shmem[0], 1.0e-4f);
const float scale = exp2(ceil(log2(amax / 448.0f)));
if (tiitg < 64) {
const float q = dsv4_e4m3fn_dequant(clamp(v / scale, -448.0f, 448.0f)) * scale;
attn_row[off + tiitg] = q;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
threadgroup_barrier(mem_flags::mem_device);
if (tiitg < 128) {
device const float4 * x4 = (device const float4 *)attn_row;
device half4 * o4 = (device half4 *)attn_cache;
const float4 v = x4[tiitg];
o4[tiitg] = half4(v);
}
return;
}
if (tgpig >= 2u) {
/* Ratio-4 state shifts for both compressors (elementwise row move,
* so the flat gid mapping is bit-exact): 4*1024 attention elements
* then 4*256 indexer elements. */
const uint gid = (tgpig - 2u) * 256u + tiitg;
const uint n0 = 4u * 1024u;
if (gid < n0) {
attn_state_kv[gid] = attn_state_kv[n0 + gid];
attn_state_score[gid] = attn_state_score[n0 + gid];
return;
}
const uint gid1 = gid - n0;
const uint n1 = 4u * 256u;
if (gid1 >= n1) return;
index_state_kv[gid1] = index_state_kv[n1 + gid1];
index_state_score[gid1] = index_state_score[n1 + gid1];
return;
}
/* -------- indexer compressor row (128 floats) -------- */
{
device float4 * y4 = (device float4 *)index_row;
device const float4 * x4 = (device const float4 *)index_row;
device const float4 * w4 = (device const float4 *)index_norm_w;
if (sgitg == 0) {
shmem[tiisg] = 0.0f;
}
float sumf = 0.0f;
if (tiitg < 32) {
sumf = dot(x4[tiitg], x4[tiitg]);
}
sumf = simd_sum(sumf);
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tiisg == 0) {
shmem[sgitg] = sumf;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float total = 0.0f;
if (tiitg < 32) {
total = simd_sum(shmem[tiisg]);
}
const float mean = total / 128.0f;
const float scale = 1.0f/sqrt(mean + args.rms_eps);
if (tiitg < 32) {
y4[tiitg] = (x4[tiitg]*scale)*w4[tiitg];
}
}
threadgroup_barrier(mem_flags::mem_device);
ds4_rope_tail_pair_affine_row(rope_args,
(device const char *)index_row,
(device char *)index_row,
128 - rope_args.n_dims,
rope_args.pos0,
tiitg,
64u);
threadgroup_barrier(mem_flags::mem_device);
{
threadgroup float *vals = shmem;
threadgroup float *absbuf = shmem + 128;
if (tiitg < 128) {
vals[tiitg] = index_row[tiitg];
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (uint stride = 1u; stride < 128u; stride <<= 1u) {
if (tiitg < 128 && (tiitg & stride) == 0u) {
const uint base = (tiitg & ~(2u * stride - 1u)) + (tiitg & (stride - 1u));
const float a = vals[base];
const float b = vals[base + stride];
vals[base] = a + b;
vals[base + stride] = a - b;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
float v = 0.0f;
if (tiitg < 128) {
v = vals[tiitg] * 0.08838834764831845f;
absbuf[tiitg] = abs(v);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const uint block = tiitg >> 5u;
const uint lane = tiitg & 31u;
const uint block_base = block * 32u;
for (uint stride = 16u; stride > 0u; stride >>= 1u) {
if (tiitg < 128 && lane < stride) {
absbuf[block_base + lane] = max(absbuf[block_base + lane],
absbuf[block_base + lane + stride]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
if (tiitg < 128) {
const float amax = max(absbuf[block_base], 7.052966104933725e-38f);
const float scale = exp2(ceil(log2(amax / 6.0f)));
index_row[tiitg] = dsv4_e2m1fn_dequant(clamp(v / scale, -6.0f, 6.0f)) * scale;
}
}
}
// Host-visible packed FlashAttention + exact inverse-RoPE decode kernel.
kernel void kernel_dsv4_flash_attn_vec_packed32_reduce_rope_f16_dk512_dv512(
constant ds4_metal_args_flash_attn_ext_vec & args [[buffer(0)]],
device const char * q [[buffer(1)]],
device const char * k [[buffer(2)]],
device const char * v [[buffer(3)]],
device const char * mask [[buffer(4)]],
device const char * sinks [[buffer(5)]],
device const char * pad [[buffer(6)]],
device char * dst [[buffer(7)]],
constant ds4_metal_args_dsv4_rope_affine_pair & rope
[[buffer(8)]],
threadgroup char * shmem [[threadgroup(0)]],
uint head [[threadgroup_position_in_grid]],
ushort tiitg [[thread_index_in_threadgroup]],
ushort tiisg [[thread_index_in_simdgroup]],
ushort sgitg [[simdgroup_index_in_threadgroup]]) {
/* Uniform specialization guard; host applies the same eligibility gate. */
if (!FC_flash_attn_ext_vec_has_mask ||
!FC_flash_attn_ext_vec_has_sinks ||
FC_flash_attn_ext_vec_has_bias ||
FC_flash_attn_ext_vec_has_scap ||
FC_flash_attn_ext_vec_nsg != 1 ||
FC_flash_attn_ext_vec_nwg != 32 ||
FC_flash_attn_ext_vec_ns10 != 512 ||
FC_flash_attn_ext_vec_ns20 != 512 ||
args.ne01 != 1 || args.ne02 != 64 || args.ne03 != 1 ||
args.ne_12_2 != 1 || args.ne_12_3 != 1 ||
args.ne31 != 1 || args.ne32 != 1 || args.ne33 != 1 ||
args.ne11 <= 0 || args.ne11 > 1024 || head >= (uint)args.ne02 ||
args.nb02 != 2048 || args.nb11 != 1024 || args.nb21 != 1024 ||
rope.head_dim != 512 || rope.n_dims != 64 ||
rope.row_bytes != 2048 || rope.inverse == 0) {
return;
}
ds4_flash_attn_vec_packed8_reduce_f16_512(
args, q, k, v, mask, sinks, pad, dst, shmem,
head, tiisg, sgitg);
/* Same producer/consumer boundary as the current reduce+RoPE kernel. */
threadgroup_barrier(mem_flags::mem_device);
const int n_nope = rope.head_dim - rope.n_dims;
device char * row = dst + (uint64_t)head * rope.row_bytes;
ds4_rope_tail_pair_affine_row(rope,
(device const char *)row,
row,
n_nope,
rope.pos0,
tiitg,
32u * 32u);
}
+267 -16
View File
@@ -1398,24 +1398,26 @@ constant int32_t FC_flash_attn_ext_vec_reduce_NWG [[function_constant(FC_FLASH_A
// Reduces split-K decode FlashAttention partials. It combines each workgroup's // Reduces split-K decode FlashAttention partials. It combines each workgroup's
// output vector and softmax (sum,max) pair into the final attention result. // output vector and softmax (sum,max) pair into the final attention result.
kernel void kernel_flash_attn_ext_vec_reduce( /* Shared and deliberately noinline so the split-K reduction is compiled once and
* every caller gets identical codegen. The RoPE-fused sibling in dsv4_rope.metal
* calls this same body, which is what keeps the fusion bit-exact. */
static __attribute__((noinline)) void ds4_flash_attn_vec_reduce_row(
constant ds4_metal_args_flash_attn_ext_vec_reduce & args, constant ds4_metal_args_flash_attn_ext_vec_reduce & args,
device const char * htmp, device const char * htmp,
device char * dst, device char * dst,
uint tgpig[[threadgroup_position_in_grid]], uint tgpig,
ushort tiisg[[thread_index_in_simdgroup]], ushort tiisg,
ushort sgitg[[simdgroup_index_in_threadgroup]]) { ushort sgitg,
#define NWG (FC_flash_attn_ext_vec_reduce_NWG) short NWG_,
#define DV (FC_flash_attn_ext_vec_reduce_DV) short DV_) {
const uint64_t rid = tgpig; const uint64_t rid = tgpig;
const short iwg = tiisg; const short iwg = tiisg;
device const float * ss = (device const float *) htmp + (uint64_t)args.nrows*DV*NWG; device const float * ss = (device const float *) htmp + (uint64_t)args.nrows*DV_*NWG_;
float S = ss[rid*(2*NWG) + 2*iwg + 0]; float S = ss[rid*(2*NWG_) + 2*iwg + 0];
float M = ss[rid*(2*NWG) + 2*iwg + 1]; float M = ss[rid*(2*NWG_) + 2*iwg + 1];
const float m = simd_max(M); const float m = simd_max(M);
const float ms = exp(M - m); const float ms = exp(M - m);
@@ -1423,19 +1425,268 @@ kernel void kernel_flash_attn_ext_vec_reduce(
S = simd_sum(S*ms); S = simd_sum(S*ms);
S = S == 0.0f ? 0.0f : 1.0f/S; S = S == 0.0f ? 0.0f : 1.0f/S;
const short DV4 = DV/4; const short DV4 = DV_/4;
device const float4 * htmp4 = (device const float4 *) htmp + rid*DV4*NWG; device const float4 * htmp4 = (device const float4 *) htmp + rid*DV4*NWG_;
device float4 * dst4 = (device float4 *) dst + rid*DV4; device float4 * dst4 = (device float4 *) dst + rid*DV4;
for (short i = sgitg; i < DV4; i += NWG) { for (short i = sgitg; i < DV4; i += NWG_) {
const float4 v = simd_sum(htmp4[i*NWG + iwg]*ms); const float4 v = simd_sum(htmp4[i*NWG_ + iwg]*ms);
if (iwg == 0) { if (iwg == 0) {
dst4[i] = v*S; dst4[i] = v*S;
} }
} }
}
#undef NWG kernel void kernel_flash_attn_ext_vec_reduce(
#undef DV constant ds4_metal_args_flash_attn_ext_vec_reduce & args,
device const char * htmp,
device char * dst,
uint tgpig[[threadgroup_position_in_grid]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]]) {
ds4_flash_attn_vec_reduce_row(args, htmp, dst, tgpig, tiisg, sgitg,
(short)FC_flash_attn_ext_vec_reduce_NWG,
(short)FC_flash_attn_ext_vec_reduce_DV);
}
// M5 decode specialization: time-slice all 32 split-K workgroups through eight
// physical simdgroups, then reduce through the same 32-lane topology without a
// device partial buffer. The host gate fixes the exact F16 512-wide geometry.
static inline void ds4_flash_attn_vec_packed8_reduce_f16_512(
constant ds4_metal_args_flash_attn_ext_vec & args,
device const char * q,
device const char * k,
device const char * v,
device const char * mask,
device const char * sinks,
device const char * pad,
device char * dst,
threadgroup char * shmem,
uint head,
ushort tiisg,
ushort sgitg) {
constexpr short NW = 32;
constexpr short C = 32;
constexpr short NSG = 8;
constexpr short NWG = 32;
constexpr short DK4 = 128;
constexpr short DV4 = 128;
constexpr short SH = 128;
/* 24,448 dynamic bytes: shared Q, eight score/mask banks, all 32
* split-local weights and stats, sink scales, and a padded 32x33 F32
* float4 partial plane. */
threadgroup half4 *q_shared = (threadgroup half4 *)shmem;
threadgroup half *score_banks =
(threadgroup half *)(q_shared + DK4);
threadgroup volatile float *weights =
(threadgroup volatile float *)(score_banks + NSG * SH);
threadgroup volatile float *stats = weights + NWG * C;
threadgroup volatile float *sink_scale = stats + 2 * NWG;
threadgroup volatile float4 *partial_plane =
(threadgroup volatile float4 *)(sink_scale + NWG);
const short lane = (short)tiisg;
threadgroup half *bank = score_banks + (short)sgitg * SH;
threadgroup float *ss = (threadgroup float *)bank;
threadgroup half *sm = bank + 2 * C;
device const float4 *q4 =
(device const float4 *)(q + (uint64_t)head * args.nb02);
if (sgitg == 0) {
for (short i = lane; i < DK4; i += NW) {
q_shared[i] = (half4)q4[i];
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
/* Eight physical simdgroups time-slice the exact 32 legacy split-K
* workgroups. The official <=1024-key gate gives each virtual split at
* most one 32-row block, so its value partial can be formed later from
* these materialized weights without changing online-softmax order. */
for (short iwg = (short)sgitg; iwg < NWG; iwg += NSG) {
float S = 0.0f;
float M = -FLT_MAX / 2;
float out_scale = 1.0f;
const int ic_original = (int)iwg * C;
weights[(uint)iwg * C + (uint)lane] = 0.0f;
ss[lane] = 0.0f;
sm[lane] = (half)0.0h;
simdgroup_barrier(mem_flags::mem_threadgroup);
if (ic_original < args.ne11) {
device const char *k_block = k;
device const char *v_block = v;
device const half *pm = (device const half *)mask;
int ic = ic_original;
if (FC_flash_attn_ext_vec_has_kvpad && ic + C > args.ne11) {
k_block = pad;
const uint64_t k_pad_bytes =
args.nb11 * (uint64_t)C *
(uint64_t)args.ne_12_2 * (uint64_t)args.ne_12_3;
const uint64_t v_pad_bytes =
args.nb21 * (uint64_t)C *
(uint64_t)args.ne_12_2 * (uint64_t)args.ne_12_3;
if (FC_flash_attn_ext_vec_shared_kvpad) {
v_block = k_block;
pm = (device const half *)(k_block +
k_pad_bytes + v_pad_bytes);
} else {
v_block = k_block + k_pad_bytes;
pm = (device const half *)(v_block + v_pad_bytes);
}
ic = 0;
}
sm[lane] = pm[ic + lane];
if (simd_max(sm[lane]) > -MAXHALF) {
device const half4 *pk4 =
(device const half4 *)(k_block +
(uint64_t)ic * args.nb11);
threadgroup const half4 *pq4 = q_shared;
pk4 += lane;
pq4 += lane;
float lane_mqk = 0.0f;
FOR_UNROLL (short cc = 0; cc < C; ++cc) {
float mqk = 0.0f;
FOR_UNROLL (short ii = 0; ii < DK4 / NW; ++ii) {
mqk += dot((float4)pk4[cc * DK4 + ii * NW],
(float4)pq4[ii * NW]);
}
mqk = simd_sum(mqk);
if (lane == cc) {
lane_mqk = mqk;
}
}
ss[lane] = fma(lane_mqk, args.scale,
(float)sm[lane]);
simdgroup_barrier(mem_flags::mem_threadgroup);
const float old_m = M;
const float score = ss[lane];
M = simd_max(max(M, score));
const float ms = exp(old_m - M);
const float vs = exp(score - M);
S = S * ms + simd_sum(vs);
ss[lane] = vs;
simdgroup_barrier(mem_flags::mem_threadgroup);
weights[(uint)iwg * C + (uint)lane] = ss[lane];
}
if (FC_flash_attn_ext_vec_has_sinks && iwg == 0) {
const float old_m = M;
const float sink = lane == 0
? ((device const float *)sinks)[head]
: -FLT_MAX / 2;
M = simd_max(max(M, sink));
const float ms = exp(old_m - M);
const float vs = exp(sink - M);
S = S * ms + simd_sum(vs);
out_scale = ms;
}
} else if (FC_flash_attn_ext_vec_has_sinks && iwg == 0) {
const float old_m = M;
const float sink = lane == 0
? ((device const float *)sinks)[head]
: -FLT_MAX / 2;
M = simd_max(max(M, sink));
const float ms = exp(old_m - M);
const float vs = exp(sink - M);
S = S * ms + simd_sum(vs);
out_scale = ms;
}
if (lane == 0) {
stats[2 * (uint)iwg + 0] = S;
stats[2 * (uint)iwg + 1] = M;
sink_scale[(uint)iwg] = out_scale;
}
simdgroup_barrier(mem_flags::mem_threadgroup);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
/* Recreate the legacy 32-lane reducer exactly: lane is the virtual split
* index, including neutral idle splits in their original tree positions. */
const short split = lane;
float reduce_S = stats[2 * (uint)split + 0];
float reduce_M = stats[2 * (uint)split + 1];
const float reduce_max = simd_max(reduce_M);
const float reduce_ms = exp(reduce_M - reduce_max);
reduce_S = simd_sum(reduce_S * reduce_ms);
const float reduce_inv =
reduce_S == 0.0f ? 0.0f : 1.0f / reduce_S;
device float4 *dst4 =
(device float4 *)(dst +
(uint64_t)head * 512u * sizeof(float));
/* Form one 32-float4 output quadrant at a time. During production each
* physical simdgroup time-slices four virtual splits while SIMD lanes are
* contiguous output columns, exactly matching the legacy V loads and
* cc-major accumulation. A padded 33-column plane avoids a 32-way TG-bank
* conflict when the reducer transposes lanes back to virtual splits. */
for (short quadrant = 0; quadrant < 4; ++quadrant) {
for (short iwg = (short)sgitg; iwg < NWG; iwg += NSG) {
float4 lo = float4(0.0f);
const int ic_original = (int)iwg * C;
if (ic_original < args.ne11) {
device const char *v_block = v;
int ic = ic_original;
if (FC_flash_attn_ext_vec_has_kvpad && ic + C > args.ne11) {
device const char *k_block = pad;
const uint64_t k_pad_bytes =
args.nb11 * (uint64_t)C *
(uint64_t)args.ne_12_2 * (uint64_t)args.ne_12_3;
if (FC_flash_attn_ext_vec_shared_kvpad) {
v_block = k_block;
} else {
v_block = k_block + k_pad_bytes;
}
ic = 0;
}
device const half4 *pv4 =
(device const half4 *)(v_block +
(uint64_t)ic * args.nb21);
threadgroup volatile float *split_weights =
weights + (uint)iwg * C;
const short oc = quadrant * NW + lane;
FOR_UNROLL (short cc = 0; cc < C; ++cc) {
lo += float4(pv4[cc * DV4 + oc]) *
float4(split_weights[cc]);
}
float4 acc = float4(0.0f);
acc += lo;
if (iwg == 0) {
acc *= sink_scale[0];
}
lo = acc;
}
partial_plane[(uint)iwg * 33u + (uint)lane] = lo;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
/* lane is now the legacy split index and each physical simdgroup
* reduces four output columns through the identical simd_sum tree. */
for (short out_lane = (short)sgitg; out_lane < NW; out_lane += NSG) {
const float4 materialized =
(float4)partial_plane[(uint)lane * 33u + (uint)out_lane];
const float4 reduced = simd_sum(materialized * reduce_ms);
if (lane == 0) {
dst4[quadrant * NW + out_lane] = reduced * reduce_inv;
}
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
} }
+143
View File
@@ -0,0 +1,143 @@
// BF16 model-weight kernels used by GLM-5.3 Flash.
static inline float glm53_bf16_to_f32(ushort value) {
return as_type<float>((uint)value << 16);
}
struct glm53_bf16_matmul_args {
uint in_dim;
uint out_dim;
uint n_rows;
};
kernel void kernel_glm53_embedding_bf16(
constant glm53_bf16_matmul_args &args,
device const ushort *weights,
device const int *tokens,
device float *out,
uint2 gid [[thread_position_in_grid]]) {
const uint d = gid.x;
const uint row = gid.y;
if (d >= args.in_dim || row >= args.n_rows) return;
const int token = tokens[row];
out[(ulong)row * args.in_dim + d] =
token >= 0 && (uint)token < args.out_dim
? glm53_bf16_to_f32(weights[(ulong)(uint)token * args.in_dim + d])
: 0.0f;
}
static inline void glm53_mul_mv_bf16_f32_row(
constant glm53_bf16_matmul_args &args,
device const ushort *weights,
device const float *x,
device float *out,
uint2 tgpig,
ushort lane,
ushort sg,
ushort nsg) {
const uint out_row = tgpig.x * (uint)nsg + sg;
const uint token = tgpig.y;
if (out_row >= args.out_dim || token >= args.n_rows) return;
device const ushort *w = weights + (ulong)out_row * args.in_dim;
device const float *xr = x + (ulong)token * args.in_dim;
float sum = 0.0f;
uint k = lane;
for (; k + 224u < args.in_dim; k += 256u) {
const ushort w0 = w[k];
const ushort w1 = w[k + 32u];
const ushort w2 = w[k + 64u];
const ushort w3 = w[k + 96u];
const ushort w4 = w[k + 128u];
const ushort w5 = w[k + 160u];
const ushort w6 = w[k + 192u];
const ushort w7 = w[k + 224u];
const float x0 = xr[k];
const float x1 = xr[k + 32u];
const float x2 = xr[k + 64u];
const float x3 = xr[k + 96u];
const float x4 = xr[k + 128u];
const float x5 = xr[k + 160u];
const float x6 = xr[k + 192u];
const float x7 = xr[k + 224u];
sum = fma(glm53_bf16_to_f32(w0), x0, sum);
sum = fma(glm53_bf16_to_f32(w1), x1, sum);
sum = fma(glm53_bf16_to_f32(w2), x2, sum);
sum = fma(glm53_bf16_to_f32(w3), x3, sum);
sum = fma(glm53_bf16_to_f32(w4), x4, sum);
sum = fma(glm53_bf16_to_f32(w5), x5, sum);
sum = fma(glm53_bf16_to_f32(w6), x6, sum);
sum = fma(glm53_bf16_to_f32(w7), x7, sum);
}
for (; k < args.in_dim; k += 32u) {
sum = fma(glm53_bf16_to_f32(w[k]), xr[k], sum);
}
sum = simd_sum(sum);
if (lane == 0u) out[(ulong)token * args.out_dim + out_row] = sum;
}
/* One simdgroup owns one output row. Eight independent loads expose enough
* memory-level parallelism for decode without changing the reduction tree. */
kernel void kernel_glm53_mul_mv_bf16_f32(
constant glm53_bf16_matmul_args &args,
device const ushort *weights,
device const float *x,
device float *out,
uint2 tgpig [[threadgroup_position_in_grid]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]],
ushort nsg [[simdgroups_per_threadgroup]]) {
glm53_mul_mv_bf16_f32_row(args, weights, x, out,
tgpig, lane, sg, nsg);
}
kernel void kernel_glm53_mul_mv_bf16_f32_qkv(
constant glm53_bf16_matmul_args &args,
device const ushort *weights_q,
device const ushort *weights_k,
device const ushort *weights_v,
device const float *x,
device float *out_q,
device float *out_k,
device float *out_v,
uint3 tgpig [[threadgroup_position_in_grid]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]],
ushort nsg [[simdgroups_per_threadgroup]]) {
device const ushort *weights = tgpig.z == 0u ? weights_q :
(tgpig.z == 1u ? weights_k : weights_v);
device float *out = tgpig.z == 0u ? out_q :
(tgpig.z == 1u ? out_k : out_v);
glm53_mul_mv_bf16_f32_row(args, weights, x, out,
tgpig.xy, lane, sg, nsg);
}
struct glm53_bf16_block16 {
ushort v[16];
};
template <typename type4x4>
void glm53_dequantize_bf16(
device const glm53_bf16_block16 *src,
short il,
thread type4x4 &reg) {
(void)il;
float4x4 values;
for (short i = 0; i < 16; i++) {
values[i / 4][i % 4] = glm53_bf16_to_f32(src->v[i]);
}
reg = (type4x4)values;
}
typedef decltype(kernel_mul_mm<
half, half4x4, simdgroup_half8x8,
half, half2x4, simdgroup_half8x8,
glm53_bf16_block16, 1, glm53_dequantize_bf16,
float, float4x4, float, float2x4>) glm53_mul_mm_bf16_t;
template [[host_name("kernel_glm53_mul_mm_bf16_f32")]]
kernel glm53_mul_mm_bf16_t kernel_mul_mm<
half, half4x4, simdgroup_half8x8,
half, half2x4, simdgroup_half8x8,
glm53_bf16_block16, 1, glm53_dequantize_bf16,
half, half4x4, float, float2x4>;
+314
View File
@@ -0,0 +1,314 @@
// Kimi Delta Attention kernels, adapted from the kimi-k3 branch.
struct glm53_kda_args {
uint n_heads;
uint n_rows;
float lower_bound;
float norm_eps;
};
/*
* One threadgroup owns one (sequence, head). Four simdgroups update four
* value rows concurrently; every lane owns four adjacent key columns.
*/
kernel void kernel_glm53_kda_decode(
constant glm53_kda_args &args,
device const float *q_in,
device const float *k_in,
device const float *v_in,
device const float *raw_gate,
device const float *raw_beta,
device const float *output_gate,
device const float *q_conv,
device const float *k_conv,
device const float *v_conv,
device const float *a_log,
device const float *dt_bias,
device const float *output_norm,
device float *conv_state,
device float *state,
device float *out,
threadgroup float *scratch [[threadgroup(0)]],
uint2 tgpig [[threadgroup_position_in_grid]],
ushort tid [[thread_index_in_threadgroup]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]]) {
constexpr uint D = 128u;
constexpr uint HISTORY = 3u;
const uint row = tgpig.x;
const uint head = tgpig.y;
if (row >= args.n_rows || head >= args.n_heads) return;
threadgroup float *sq = scratch;
threadgroup float *sk = sq + D;
threadgroup float *sd = sk + D;
threadgroup float *sv = sd + D;
threadgroup float *so = sv + D;
threadgroup float *reduce_q = so + D;
threadgroup float *reduce_k = reduce_q + 4u;
threadgroup float *reduce_o = reduce_k + 4u;
threadgroup float *beta_shared = reduce_o + 4u;
const uint projection = args.n_heads * D;
const uint channel = head * D + tid;
const ulong input_base = (ulong)row * projection + head * D;
const ulong conv_row_stride = 3ul * HISTORY * projection;
if (tid < D) {
float q_acc = 0.0f;
float k_acc = 0.0f;
float v_acc = 0.0f;
device float *q_state = conv_state +
(ulong)row * conv_row_stride;
device float *k_state = q_state + HISTORY * projection;
device float *v_state = k_state + HISTORY * projection;
for (uint w = 0; w < HISTORY; w++) {
q_acc = fma(q_state[(ulong)w * projection + channel],
q_conv[(ulong)channel * 4u + w], q_acc);
k_acc = fma(k_state[(ulong)w * projection + channel],
k_conv[(ulong)channel * 4u + w], k_acc);
v_acc = fma(v_state[(ulong)w * projection + channel],
v_conv[(ulong)channel * 4u + w], v_acc);
}
const float q_new = q_in[input_base + tid];
const float k_new = k_in[input_base + tid];
const float v_new = v_in[input_base + tid];
q_acc = fma(q_new, q_conv[(ulong)channel * 4u + 3u], q_acc);
k_acc = fma(k_new, k_conv[(ulong)channel * 4u + 3u], k_acc);
v_acc = fma(v_new, v_conv[(ulong)channel * 4u + 3u], v_acc);
q_state[channel] = q_state[projection + channel];
q_state[projection + channel] = q_state[2ul * projection + channel];
q_state[2ul * projection + channel] = q_new;
k_state[channel] = k_state[projection + channel];
k_state[projection + channel] = k_state[2ul * projection + channel];
k_state[2ul * projection + channel] = k_new;
v_state[channel] = v_state[projection + channel];
v_state[projection + channel] = v_state[2ul * projection + channel];
v_state[2ul * projection + channel] = v_new;
sq[tid] = q_acc / (1.0f + exp(-q_acc));
sk[tid] = k_acc / (1.0f + exp(-k_acc));
sv[tid] = v_acc / (1.0f + exp(-v_acc));
const float gate = raw_gate[input_base + tid] + dt_bias[channel];
sd[tid] = exp(args.lower_bound *
(1.0f / (1.0f + exp(-exp(a_log[head]) * gate))));
}
if (tid == 0u) {
beta_shared[0] =
1.0f / (1.0f + exp(-raw_beta[(ulong)row * args.n_heads + head]));
}
threadgroup_barrier(mem_flags::mem_threadgroup |
mem_flags::mem_device);
float q_sumsq = sq[tid] * sq[tid];
float k_sumsq = sk[tid] * sk[tid];
q_sumsq = simd_sum(q_sumsq);
k_sumsq = simd_sum(k_sumsq);
if (lane == 0u) {
reduce_q[sg] = q_sumsq;
reduce_k[sg] = k_sumsq;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float q_total = lane < 4u ? reduce_q[lane] : 0.0f;
float k_total = lane < 4u ? reduce_k[lane] : 0.0f;
q_total = simd_sum(q_total);
k_total = simd_sum(k_total);
const float q_scale = rsqrt(q_total + 1.0e-6f) * 0x1.6a09e6p-4f;
const float k_scale = rsqrt(k_total + 1.0e-6f);
if (tid < D) {
sq[tid] *= q_scale;
sk[tid] *= k_scale;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const uint k0 = lane * 4u;
const float4 q4 = *((threadgroup float4 *)(sq + k0));
const float4 k4 = *((threadgroup float4 *)(sk + k0));
const float4 decay4 = *((threadgroup float4 *)(sd + k0));
const ulong state_head =
((ulong)row * args.n_heads + head) * D * D;
for (uint value = sg; value < D; value += 4u) {
device float4 *hptr =
(device float4 *)(state + state_head + (ulong)value * D + k0);
float4 h = *hptr * decay4;
float hk = dot(h, k4);
hk = simd_sum(hk);
const float delta_v = (sv[value] - hk) * beta_shared[0];
h = fma(k4, float4(delta_v), h);
*hptr = h;
float hq = simd_sum(dot(h, q4));
if (lane == 0u) so[value] = hq;
}
threadgroup_barrier(mem_flags::mem_threadgroup |
mem_flags::mem_device);
float o_sumsq = so[tid] * so[tid];
o_sumsq = simd_sum(o_sumsq);
if (lane == 0u) reduce_o[sg] = o_sumsq;
threadgroup_barrier(mem_flags::mem_threadgroup);
float o_total = lane < 4u ? reduce_o[lane] : 0.0f;
o_total = simd_sum(o_total);
const float o_scale = rsqrt(o_total / (float)D + args.norm_eps);
if (tid < D) {
const ulong index = input_base + tid;
const float gate =
1.0f / (1.0f + exp(-output_gate[index]));
out[index] = so[tid] * o_scale * output_norm[tid] * gate;
}
}
kernel void kernel_glm53_kda_prefill_prepare(
constant glm53_kda_args &args,
device float *q,
device float *k,
device float *v,
device float *raw_gate,
device const float *q_conv,
device const float *k_conv,
device const float *v_conv,
device const float *a_log,
device const float *dt_bias,
device float *conv_state,
threadgroup float *scratch [[threadgroup(0)]],
uint head [[threadgroup_position_in_grid]],
ushort tid [[thread_index_in_threadgroup]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]]) {
constexpr uint D = 128u;
constexpr uint HISTORY = 3u;
if (head >= args.n_heads) return;
threadgroup float *sq = scratch;
threadgroup float *sk = sq + D;
threadgroup float *reduce_q = sk + D;
threadgroup float *reduce_k = reduce_q + 4u;
const uint projection = args.n_heads * D;
const uint channel = head * D + tid;
device float *q_state = conv_state;
device float *k_state = q_state + HISTORY * projection;
device float *v_state = k_state + HISTORY * projection;
for (uint token = 0; token < args.n_rows; token++) {
const ulong index = (ulong)token * projection + channel;
float q_acc = 0.0f;
float k_acc = 0.0f;
float v_acc = 0.0f;
for (uint w = 0; w < HISTORY; w++) {
q_acc = fma(q_state[(ulong)w * projection + channel],
q_conv[(ulong)channel * 4u + w], q_acc);
k_acc = fma(k_state[(ulong)w * projection + channel],
k_conv[(ulong)channel * 4u + w], k_acc);
v_acc = fma(v_state[(ulong)w * projection + channel],
v_conv[(ulong)channel * 4u + w], v_acc);
}
const float q_new = q[index];
const float k_new = k[index];
const float v_new = v[index];
q_acc = fma(q_new, q_conv[(ulong)channel * 4u + 3u], q_acc);
k_acc = fma(k_new, k_conv[(ulong)channel * 4u + 3u], k_acc);
v_acc = fma(v_new, v_conv[(ulong)channel * 4u + 3u], v_acc);
q_state[channel] = q_state[projection + channel];
q_state[projection + channel] = q_state[2ul * projection + channel];
q_state[2ul * projection + channel] = q_new;
k_state[channel] = k_state[projection + channel];
k_state[projection + channel] = k_state[2ul * projection + channel];
k_state[2ul * projection + channel] = k_new;
v_state[channel] = v_state[projection + channel];
v_state[projection + channel] = v_state[2ul * projection + channel];
v_state[2ul * projection + channel] = v_new;
sq[tid] = q_acc / (1.0f + exp(-q_acc));
sk[tid] = k_acc / (1.0f + exp(-k_acc));
v[index] = v_acc / (1.0f + exp(-v_acc));
const float gate = raw_gate[index] + dt_bias[channel];
raw_gate[index] = exp(args.lower_bound *
(1.0f / (1.0f + exp(-exp(a_log[head]) * gate))));
threadgroup_barrier(mem_flags::mem_threadgroup |
mem_flags::mem_device);
float q_sumsq = simd_sum(sq[tid] * sq[tid]);
float k_sumsq = simd_sum(sk[tid] * sk[tid]);
if (lane == 0u) {
reduce_q[sg] = q_sumsq;
reduce_k[sg] = k_sumsq;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
float q_total = lane < 4u ? reduce_q[lane] : 0.0f;
float k_total = lane < 4u ? reduce_k[lane] : 0.0f;
q_total = simd_sum(q_total);
k_total = simd_sum(k_total);
q[index] = sq[tid] * rsqrt(q_total + 1.0e-6f) *
0x1.6a09e6p-4f;
k[index] = sk[tid] * rsqrt(k_total + 1.0e-6f);
threadgroup_barrier(mem_flags::mem_threadgroup |
mem_flags::mem_device);
}
}
kernel void kernel_glm53_kda_prefill_recurrence(
constant glm53_kda_args &args,
device const float *q,
device const float *k,
device const float *v,
device const float *decay,
device const float *raw_beta,
device float *state,
device float *out,
uint2 tgpig [[threadgroup_position_in_grid]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]]) {
constexpr uint D = 128u;
const uint head = tgpig.x;
const uint value = tgpig.y * 4u + sg;
if (head >= args.n_heads || value >= D) return;
const uint projection = args.n_heads * D;
const uint k0 = lane * 4u;
device float4 *state_ptr = (device float4 *)(
state + ((ulong)head * D + value) * D + k0);
float4 h = *state_ptr;
for (uint token = 0; token < args.n_rows; token++) {
const ulong base = (ulong)token * projection + head * D;
const float4 q4 = *((device const float4 *)(q + base + k0));
const float4 k4 = *((device const float4 *)(k + base + k0));
const float4 decay4 =
*((device const float4 *)(decay + base + k0));
h *= decay4;
const float hk = simd_sum(dot(h, k4));
const float beta = 1.0f /
(1.0f + exp(-raw_beta[(ulong)token * args.n_heads + head]));
const float delta_v = (v[base + value] - hk) * beta;
h = fma(k4, float4(delta_v), h);
const float result = simd_sum(dot(h, q4));
if (lane == 0u) out[base + value] = result;
}
*state_ptr = h;
}
kernel void kernel_glm53_kda_prefill_output(
constant glm53_kda_args &args,
device float *out,
device const float *output_gate,
device const float *output_norm,
threadgroup float *partial [[threadgroup(0)]],
uint2 tgpig [[threadgroup_position_in_grid]],
ushort tid [[thread_index_in_threadgroup]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]]) {
constexpr uint D = 128u;
const uint token = tgpig.x;
const uint head = tgpig.y;
if (token >= args.n_rows || head >= args.n_heads) return;
const uint projection = args.n_heads * D;
const ulong base = (ulong)token * projection + head * D;
const float raw = out[base + tid];
float sumsq = simd_sum(raw * raw);
if (lane == 0u) partial[sg] = sumsq;
threadgroup_barrier(mem_flags::mem_threadgroup);
float total = lane < 4u ? partial[lane] : 0.0f;
total = simd_sum(total);
const float scale = rsqrt(total / (float)D + args.norm_eps);
out[base + tid] = raw * scale * output_norm[tid] /
(1.0f + exp(-output_gate[base + tid]));
}
+271
View File
@@ -0,0 +1,271 @@
// GLM-5.3 Flash vision operations not covered by the shared BF16 matmuls.
struct glm53_vision_rows_args {
uint width;
uint rows;
float eps;
};
struct glm53_vision_qkv_args {
uint rows;
uint grid_h;
uint grid_w;
float eps;
};
struct glm53_vision_attention_args {
uint rows;
float scale;
};
struct glm53_vision_scatter_args {
uint dst_row;
uint image_row;
uint rows;
uint total_rows;
uint width;
uint hc;
};
static inline float glm53_vision_erf(float x) {
const float sign = x < 0.0f ? -1.0f : 1.0f;
const float a = abs(x);
const float t = 1.0f / (1.0f + 0.3275911f * a);
const float p = (((((1.061405429f * t - 1.453152027f) * t) +
1.421413741f) * t - 0.284496736f) * t +
0.254829592f) * t;
return sign * (1.0f - p * exp(-a * a));
}
kernel void kernel_glm53_vision_add_bias(
constant glm53_vision_rows_args &args,
device float *x,
device const ushort *bias,
uint2 gid [[thread_position_in_grid]]) {
if (gid.x >= args.width || gid.y >= args.rows) return;
x[(ulong)gid.y * args.width + gid.x] += glm53_bf16_to_f32(bias[gid.x]);
}
kernel void kernel_glm53_vision_rms_bf16(
constant glm53_vision_rows_args &args,
device const float *x,
device const ushort *weight,
device float *out,
threadgroup float *partial,
uint row [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]],
ushort nsg [[simdgroups_per_threadgroup]]) {
if (row >= args.rows) return;
device const float *xr = x + (ulong)row * args.width;
device float *yr = out + (ulong)row * args.width;
float sum = 0.0f;
for (uint d = tid; d < args.width; d += 256u) sum = fma(xr[d], xr[d], sum);
sum = simd_sum(sum);
if (lane == 0u) partial[sg] = sum;
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sg == 0u) {
float v = lane < nsg ? partial[lane] : 0.0f;
v = simd_sum(v);
if (lane == 0u) partial[0] = rsqrt(v / (float)args.width + args.eps);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const float inv = partial[0];
for (uint d = tid; d < args.width; d += 256u) {
yr[d] = xr[d] * inv * glm53_bf16_to_f32(weight[d]);
}
}
kernel void kernel_glm53_vision_qkv_rope(
constant glm53_vision_qkv_args &args,
device const float *qkv,
device const ushort *bias,
device const ushort *q_weight,
device const ushort *k_weight,
device float *q,
device float *k,
device float *v,
uint2 group [[threadgroup_position_in_grid]],
ushort lane [[thread_index_in_simdgroup]]) {
const uint row = group.x;
const uint head = group.y;
if (row >= args.rows || head >= 16u) return;
const ulong qkv_base = (ulong)row * 3072u + (ulong)head * 64u;
const ulong out_base = (ulong)row * 1024u + (ulong)head * 64u;
float q0 = qkv[qkv_base + lane] + glm53_bf16_to_f32(bias[(ulong)head * 64u + lane]);
float q1 = qkv[qkv_base + lane + 32u] +
glm53_bf16_to_f32(bias[(ulong)head * 64u + lane + 32u]);
float k0 = qkv[qkv_base + 1024u + lane] +
glm53_bf16_to_f32(bias[1024u + (ulong)head * 64u + lane]);
float k1 = qkv[qkv_base + 1024u + lane + 32u] +
glm53_bf16_to_f32(bias[1024u + (ulong)head * 64u + lane + 32u]);
const float qsum = simd_sum(fma(q0, q0, q1 * q1));
const float ksum = simd_sum(fma(k0, k0, k1 * k1));
const float qinv = rsqrt(qsum / 64.0f + args.eps);
const float kinv = rsqrt(ksum / 64.0f + args.eps);
q0 *= qinv * glm53_bf16_to_f32(q_weight[lane]);
q1 *= qinv * glm53_bf16_to_f32(q_weight[lane + 32u]);
k0 *= kinv * glm53_bf16_to_f32(k_weight[lane]);
k1 *= kinv * glm53_bf16_to_f32(k_weight[lane + 32u]);
const uint merge_w = args.grid_w / 2u;
const uint group_index = row / 4u;
const uint within = row & 3u;
const uint py = (group_index / merge_w) * 2u + within / 2u;
const uint px = (group_index % merge_w) * 2u + within % 2u;
const uint freq_index = lane & 15u;
const uint pos = lane < 16u ? py : px;
const float inv_freq = powr(10000.0f, -(float)freq_index / 16.0f);
const float angle = (float)pos * inv_freq;
const float cs = cos(angle);
const float sn = sin(angle);
q[out_base + lane] = q0 * cs - q1 * sn;
q[out_base + lane + 32u] = q1 * cs + q0 * sn;
k[out_base + lane] = k0 * cs - k1 * sn;
k[out_base + lane + 32u] = k1 * cs + k0 * sn;
v[out_base + lane] = qkv[qkv_base + 2048u + lane] +
glm53_bf16_to_f32(bias[2048u + (ulong)head * 64u + lane]);
v[out_base + lane + 32u] = qkv[qkv_base + 2048u + lane + 32u] +
glm53_bf16_to_f32(bias[2048u + (ulong)head * 64u + lane + 32u]);
}
/* A simdgroup owns one query/head and keeps its 64 output values in registers.
* This is quadratic in compute, as the model graph requires, but linear in
* memory and never materializes the attention matrix. */
kernel void kernel_glm53_vision_attention(
constant glm53_vision_attention_args &args,
device const float *q,
device const float *k,
device const float *v,
device float *out,
uint2 group [[threadgroup_position_in_grid]],
ushort lane [[thread_index_in_simdgroup]]) {
const uint row = group.x;
const uint head = group.y;
if (row >= args.rows || head >= 16u) return;
const ulong base = (ulong)row * 1024u + (ulong)head * 64u;
const float q0 = q[base + lane];
const float q1 = q[base + lane + 32u];
float acc0 = 0.0f, acc1 = 0.0f;
float max_score = -INFINITY;
float denom = 0.0f;
for (uint key_row = 0; key_row < args.rows; key_row++) {
const ulong kb = (ulong)key_row * 1024u + (ulong)head * 64u;
float score = simd_sum(q0 * k[kb + lane] + q1 * k[kb + lane + 32u]);
score *= args.scale;
const float next_max = max(max_score, score);
const float old_scale = max_score == -INFINITY ? 0.0f : exp(max_score - next_max);
const float new_scale = exp(score - next_max);
denom = denom * old_scale + new_scale;
acc0 = acc0 * old_scale + new_scale * v[kb + lane];
acc1 = acc1 * old_scale + new_scale * v[kb + lane + 32u];
max_score = next_max;
}
out[base + lane] = acc0 / denom;
out[base + lane + 32u] = acc1 / denom;
}
kernel void kernel_glm53_vision_bias_residual(
constant glm53_vision_rows_args &args,
device float *x,
device const ushort *bias,
device const float *residual,
uint2 gid [[thread_position_in_grid]]) {
if (gid.x >= args.width || gid.y >= args.rows) return;
const ulong off = (ulong)gid.y * args.width + gid.x;
x[off] += glm53_bf16_to_f32(bias[gid.x]) + residual[off];
}
kernel void kernel_glm53_vision_swiglu_bias(
constant glm53_vision_rows_args &args,
device const float *gate,
device const ushort *gate_bias,
device const float *up,
device const ushort *up_bias,
device float *out,
uint2 gid [[thread_position_in_grid]]) {
if (gid.x >= args.width || gid.y >= args.rows) return;
const ulong off = (ulong)gid.y * args.width + gid.x;
const float g = min(gate[off] + glm53_bf16_to_f32(gate_bias[gid.x]), 10.0f);
const float u = clamp(up[off] + glm53_bf16_to_f32(up_bias[gid.x]), -10.0f, 10.0f);
out[off] = (g / (1.0f + exp(-g))) * u;
}
kernel void kernel_glm53_vision_downsample_reorder(
device const float *x,
device float *out,
uint2 gid [[thread_position_in_grid]]) {
const uint d = gid.x;
const uint row = gid.y;
if (d >= 4096u) return;
const uint channel = d / 4u;
const uint within = d & 3u;
out[(ulong)row * 4096u + d] = x[((ulong)row * 4u + within) * 1024u + channel];
}
kernel void kernel_glm53_vision_layernorm_gelu(
constant glm53_vision_rows_args &args,
device const float *x,
device const ushort *weight,
device const ushort *bias,
device float *out,
threadgroup float *partial,
uint row [[threadgroup_position_in_grid]],
uint tid [[thread_index_in_threadgroup]],
ushort lane [[thread_index_in_simdgroup]],
ushort sg [[simdgroup_index_in_threadgroup]],
ushort nsg [[simdgroups_per_threadgroup]]) {
if (row >= args.rows) return;
device const float *xr = x + (ulong)row * args.width;
device float *yr = out + (ulong)row * args.width;
float sum = 0.0f;
for (uint d = tid; d < args.width; d += 256u) sum += xr[d];
sum = simd_sum(sum);
if (lane == 0u) partial[sg] = sum;
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sg == 0u) {
float v = lane < nsg ? partial[lane] : 0.0f;
v = simd_sum(v);
if (lane == 0u) partial[0] = v / (float)args.width;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const float mean = partial[0];
float var = 0.0f;
for (uint d = tid; d < args.width; d += 256u) {
const float centered = xr[d] - mean;
var = fma(centered, centered, var);
}
var = simd_sum(var);
if (lane == 0u) partial[sg] = var;
threadgroup_barrier(mem_flags::mem_threadgroup);
if (sg == 0u) {
float v = lane < nsg ? partial[lane] : 0.0f;
v = simd_sum(v);
if (lane == 0u) partial[0] = rsqrt(v / (float)args.width + args.eps);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
const float inv = partial[0];
const float inv_sqrt2 = 0.7071067811865475f;
for (uint d = tid; d < args.width; d += 256u) {
float v = (xr[d] - mean) * inv * glm53_bf16_to_f32(weight[d]) +
glm53_bf16_to_f32(bias[d]);
yr[d] = 0.5f * v * (1.0f + glm53_vision_erf(v * inv_sqrt2));
}
}
kernel void kernel_glm53_vision_scatter_hc(
constant glm53_vision_scatter_args &args,
device float *hc,
device const float *image,
uint2 gid [[thread_position_in_grid]]) {
const uint d = gid.x;
const uint linear_row = gid.y;
if (d >= args.width || linear_row >= args.rows * args.hc) return;
const uint image_delta = linear_row / args.hc;
const uint hc_index = linear_row % args.hc;
const ulong dst = ((ulong)(args.dst_row + image_delta) * args.hc + hc_index) *
args.width + d;
const ulong src = (ulong)(args.image_row + image_delta) * args.width + d;
if (args.dst_row + image_delta < args.total_rows) hc[dst] = image[src];
}
+2120 -846
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"header_sha256": "2665a76463f3f6ee283c6a50b66e4a527318a114080b31441dfa900042097a39",
"clamp": {
"zero": {
"max_new": 512,
"max_start": 0,
"source_sha256": "fd56a2d3bd76649775e853a28c41bd2a290255c7efbe4df471c6ed5e557b474e",
"kernels": "[[host_name(\"Ei4IBroadcastBFi4ISubtractAEGi4IBroadcastCHi4IMaximumFGIi4IMinimumHDJi4OMinimumIG_VCCV_i4i4_13771019418134704434_contiguous\")]]\n[[kernel]] void Ei4IBroadcastBFi4ISubtractAEGi4IBroadcastCHi4IMaximumFGIi4IMinimumHDJi4OMinimumIG_VCCV_i4i4_13771019418134704434_contiguous(\n device const int32_t* A [[buffer(0)]],\n device const int32_t* B [[buffer(1)]],\n device int32_t* C [[buffer(2)]],\n constant const uint& size [[buffer(3)]],\n uint3 pos [[thread_position_in_grid]],\n uint3 grid [[threads_per_grid]]) {\n constexpr int N_ = 1;\n uint index = N_ * pos.x;\n int32_t tmp_A = A[index];\n auto tmp_D = static_cast<int32_t>(512);\n auto tmp_E = static_cast<int32_t>(0);\n int32_t tmp_B = B[index];\n int32_t tmp_F = cast_to<int32_t>(tmp_D);\n int32_t tmp_G = Subtract()(tmp_A, tmp_F);\n int32_t tmp_H = cast_to<int32_t>(tmp_E);\n int32_t tmp_I = Maximum()(tmp_G, tmp_H);\n int32_t tmp_J = Minimum()(tmp_I, tmp_B);\n int32_t tmp_C = Minimum()(tmp_J, tmp_H);\n C[index] = tmp_C;\n}\n"
},
"equal": {
"max_new": 512,
"max_start": 512,
"source_sha256": "8f21de0cf23479618a4b0729740e197e50fec09f4f87fcb0caa8b8569ba8173a",
"kernels": "[[host_name(\"Ei4IBroadcastBFi4ISubtractAEGi4IBroadcastCHi4IMaximumFGIi4IMinimumHDJi4OMinimumIE_VCCV_i4i4_13771019418134704434_contiguous\")]]\n[[kernel]] void Ei4IBroadcastBFi4ISubtractAEGi4IBroadcastCHi4IMaximumFGIi4IMinimumHDJi4OMinimumIE_VCCV_i4i4_13771019418134704434_contiguous(\n device const int32_t* A [[buffer(0)]],\n device const int32_t* B [[buffer(1)]],\n device int32_t* C [[buffer(2)]],\n constant const uint& size [[buffer(3)]],\n uint3 pos [[thread_position_in_grid]],\n uint3 grid [[threads_per_grid]]) {\n constexpr int N_ = 1;\n uint index = N_ * pos.x;\n int32_t tmp_A = A[index];\n auto tmp_D = static_cast<int32_t>(512);\n auto tmp_E = static_cast<int32_t>(0);\n int32_t tmp_B = B[index];\n int32_t tmp_F = cast_to<int32_t>(tmp_D);\n int32_t tmp_G = Subtract()(tmp_A, tmp_F);\n int32_t tmp_H = cast_to<int32_t>(tmp_E);\n int32_t tmp_I = Maximum()(tmp_G, tmp_H);\n int32_t tmp_J = Minimum()(tmp_I, tmp_B);\n int32_t tmp_C = Minimum()(tmp_J, tmp_F);\n C[index] = tmp_C;\n}\n"
},
"distinct": {
"max_new": 1,
"max_start": 1023,
"source_sha256": "15dd362325e0d9ea44a8646c29ed70c7f0ed53b1dcc9fe44b4f65fa4a11823e7",
"kernels": "[[host_name(\"Fi4IBroadcastBGi4ISubtractAFHi4IBroadcastCIi4IMaximumGHJi4IMinimumIDKi4IBroadcastELi4OMinimumJK_VCCVC_i4i4_1937821606537560661_contiguous\")]]\n[[kernel]] void Fi4IBroadcastBGi4ISubtractAFHi4IBroadcastCIi4IMaximumGHJi4IMinimumIDKi4IBroadcastELi4OMinimumJK_VCCVC_i4i4_1937821606537560661_contiguous(\n device const int32_t* A [[buffer(0)]],\n device const int32_t* B [[buffer(1)]],\n device int32_t* C [[buffer(2)]],\n constant const uint& size [[buffer(3)]],\n uint3 pos [[thread_position_in_grid]],\n uint3 grid [[threads_per_grid]]) {\n constexpr int N_ = 1;\n uint index = N_ * pos.x;\n int32_t tmp_A = A[index];\n auto tmp_D = static_cast<int32_t>(1);\n auto tmp_E = static_cast<int32_t>(0);\n int32_t tmp_B = B[index];\n auto tmp_F = static_cast<int32_t>(1023);\n int32_t tmp_G = cast_to<int32_t>(tmp_D);\n int32_t tmp_H = Subtract()(tmp_A, tmp_G);\n int32_t tmp_I = cast_to<int32_t>(tmp_E);\n int32_t tmp_J = Maximum()(tmp_H, tmp_I);\n int32_t tmp_K = Minimum()(tmp_J, tmp_B);\n int32_t tmp_L = cast_to<int32_t>(tmp_F);\n int32_t tmp_C = Minimum()(tmp_K, tmp_L);\n C[index] = tmp_C;\n}\n"
}
},
"multiply": {
"source_sha256": "1cf792edbbd886d156b68f5082563d7335278c98ad8c996d547b19fd7cf125b0",
"kernels": "[[host_name(\"Ci4IBroadcastBDi4OMultiplyAC_VC_i4_2169371982377735806_contiguous\")]]\n[[kernel]] void Ci4IBroadcastBDi4OMultiplyAC_VC_i4_2169371982377735806_contiguous(\n device const int32_t* A [[buffer(0)]],\n device int32_t* B [[buffer(1)]],\n constant const uint& size [[buffer(2)]],\n uint3 pos [[thread_position_in_grid]],\n uint3 grid [[threads_per_grid]]) {\n constexpr int N_ = 1;\n uint index = N_ * pos.x;\n int32_t tmp_A = A[index];\n auto tmp_C = static_cast<int32_t>(4);\n int32_t tmp_D = cast_to<int32_t>(tmp_C);\n int32_t tmp_B = Multiply()(tmp_A, tmp_D);\n B[index] = tmp_B;\n}\n"
},
"check_distinct": {
"max_new": 257,
"max_start": 255,
"source_sha256": "c952e6026e18bc5c5cc6f3b78852607d9efa8ce7a9b083d439515c95c1620ef9",
"kernels": "[[host_name(\"Fi4IBroadcastBGi4ISubtractAFHi4IBroadcastCIi4IMaximumGHJi4IMinimumIDKi4IBroadcastELi4OMinimumJK_VCCVC_i4i4_7252438397961030063_contiguous\")]]\n[[kernel]] void Fi4IBroadcastBGi4ISubtractAFHi4IBroadcastCIi4IMaximumGHJi4IMinimumIDKi4IBroadcastELi4OMinimumJK_VCCVC_i4i4_7252438397961030063_contiguous(\n device const int32_t* A [[buffer(0)]],\n device const int32_t* B [[buffer(1)]],\n device int32_t* C [[buffer(2)]],\n constant const uint& size [[buffer(3)]],\n uint3 pos [[thread_position_in_grid]],\n uint3 grid [[threads_per_grid]]) {\n constexpr int N_ = 1;\n uint index = N_ * pos.x;\n int32_t tmp_A = A[index];\n auto tmp_D = static_cast<int32_t>(257);\n auto tmp_E = static_cast<int32_t>(0);\n int32_t tmp_B = B[index];\n auto tmp_F = static_cast<int32_t>(255);\n int32_t tmp_G = cast_to<int32_t>(tmp_D);\n int32_t tmp_H = Subtract()(tmp_A, tmp_G);\n int32_t tmp_I = cast_to<int32_t>(tmp_E);\n int32_t tmp_J = Maximum()(tmp_H, tmp_I);\n int32_t tmp_K = Minimum()(tmp_J, tmp_B);\n int32_t tmp_L = cast_to<int32_t>(tmp_F);\n int32_t tmp_C = Minimum()(tmp_K, tmp_L);\n C[index] = tmp_C;\n}\n"
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
+10419
View File
File diff suppressed because it is too large Load Diff
+142
View File
@@ -241,3 +241,145 @@ kernel void kernel_dsv4_qkv_rms_norm_f32_4(
y[i] = (x[i] * scale) * w[i]; y[i] = (x[i] * scale) * w[i];
} }
} }
// Decode-only triple fusion: the q/kv RMS norm, the KV RoPE tail, and the
// FP8/raw finalizer were three back-to-back dispatches on the same rows.
// The q threadgroup is byte-identical to kernel_dsv4_qkv_rms_norm_f32_4.
// The kv threadgroup continues with the shared affine-row RoPE helper (lane
// mapping preserved: r == lane on the first 64 lanes) and a verbatim copy of
// kernel_dsv4_kv_fp8_store_f32 with its work predicated to the first 64
// lanes (barriers stay uniform across the whole threadgroup). Arithmetic,
// order and rounding are unchanged; gated and verified against
// full-vocabulary logits before promotion.
kernel void kernel_dsv4_qkv_rms_norm_kv_rope_fp8_store_f32(
constant ds4_metal_args_qkv_rms_norm & args,
constant ds4_metal_args_dsv4_rope_affine_pair & rope,
constant ds4_metal_args_dsv4_kv_fp8_store & store,
device const float4 * q_src,
device const float4 * q_weight,
device float4 * q_dst,
device const float4 * kv_src,
device const float4 * kv_weight,
device float4 * kv_dst,
device float * raw_cache,
threadgroup float * shmem_f32 [[threadgroup(0)]],
uint3 tgpig[[threadgroup_position_in_grid]],
ushort3 tpitg[[thread_position_in_threadgroup]],
ushort sgitg[[simdgroup_index_in_threadgroup]],
ushort tiisg[[thread_index_in_simdgroup]],
ushort3 ntg[[threads_per_threadgroup]]) {
if (sgitg == 0) {
shmem_f32[tiisg] = 0.0f;
}
const uint row = tgpig.x;
const bool kv_task = tgpig.y != 0;
const int n = kv_task ? args.kv_n : args.q_n;
const int n4 = kv_task ? args.kv_n4 : args.q_n4;
const uint64_t row_stride4 = (kv_task ? args.kv_row_stride : args.q_row_stride) / sizeof(float4);
device const float4 * x = kv_task ? kv_src + row * row_stride4 : q_src + row * row_stride4;
device const float4 * w = kv_task ? kv_weight : q_weight;
device float4 * y = kv_task ? kv_dst + row * row_stride4 : q_dst + row * row_stride4;
float sumf = 0.0f;
for (int i = tpitg.x; i < n4; i += ntg.x) {
const float4 v = x[i];
sumf += dot(v, v);
}
sumf = simd_sum(sumf);
threadgroup_barrier(mem_flags::mem_threadgroup);
if (tiisg == 0) {
shmem_f32[sgitg] = sumf;
}
threadgroup_barrier(mem_flags::mem_threadgroup);
sumf = shmem_f32[tiisg];
sumf = simd_sum(sumf);
#ifdef DS4_METAL_NORM_RSQRT_DISABLE
const float scale = 1.0f / sqrt(sumf / float(n) + args.eps);
#else
const float scale = rsqrt(sumf / float(n) + args.eps);
#endif
for (int i = tpitg.x; i < n4; i += ntg.x) {
y[i] = (x[i] * scale) * w[i];
}
if (!kv_task) {
return;
}
// KV RoPE tail in place, then the FP8/raw finalizer (verbatim bodies).
threadgroup_barrier(mem_flags::mem_device_and_threadgroup);
device char *kv_row = (device char *)(kv_dst + row * row_stride4);
const int rope_n_nope = rope.head_dim - rope.n_dims;
if (rope_n_nope < 0) {
return;
}
ds4_rope_tail_pair_affine_row(rope,
(device const char *)kv_row,
kv_row,
rope_n_nope,
rope.pos0,
tpitg.x,
ntg.x);
threadgroup_barrier(mem_flags::mem_device_and_threadgroup);
const int head_dim = store.head_dim;
const int n_rot = store.n_rot;
const int n_nope = head_dim - n_rot;
if (head_dim <= 0 || n_rot < 0 || n_nope < 0) {
return;
}
const uint tid = tpitg.x;
device float *kv = (device float *)kv_row;
device float *raw = raw_cache + (int64_t)store.raw_row * head_dim;
threadgroup float *scratch = shmem_f32 + 32;
for (int off = 0; off < n_nope; off += 64) {
float v = 0.0f;
if (tid < 64u && off + (int)tid < n_nope) {
v = kv[off + tid];
scratch[tid] = abs(v);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
for (uint stride = 32; stride > 0; stride >>= 1) {
if (tid < stride) {
scratch[tid] = max(scratch[tid], scratch[tid + stride]);
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
const float amax = max(scratch[0], 1.0e-4f);
const float fp8_scale = exp2(ceil(log2(amax / 448.0f)));
if (tid < 64u && off + (int)tid < n_nope) {
const float q = dsv4_e4m3fn_dequant(clamp(v / fp8_scale, -448.0f, 448.0f)) * fp8_scale;
kv[off + tid] = q;
#ifdef DS4_METAL_KV_RAW_F32
raw[off + tid] = q;
#else
raw[off + tid] = (float)((half)q);
#endif
}
threadgroup_barrier(mem_flags::mem_threadgroup);
}
if (tid < 64u) {
for (int i = n_nope + tid; i < head_dim; i += 64) {
#ifdef DS4_METAL_KV_RAW_F32
raw[i] = kv[i];
#else
raw[i] = (float)((half)kv[i]);
#endif
}
}
}
+4082
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
DROP TABLE a2ui_messages;
@@ -0,0 +1,10 @@
CREATE TABLE a2ui_messages (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
json TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id),
FOREIGN KEY (message_id) REFERENCES messages(id)
);
CREATE INDEX a2ui_messages_session_id ON a2ui_messages(session_id, id);
@@ -0,0 +1 @@
ALTER TABLE a2ui_messages DROP COLUMN dismissed;
@@ -0,0 +1,2 @@
ALTER TABLE a2ui_messages
ADD COLUMN dismissed BOOLEAN NOT NULL DEFAULT 0;
@@ -0,0 +1,4 @@
ALTER TABLE messages DROP COLUMN output_tokens;
ALTER TABLE messages DROP COLUMN cached_tokens;
ALTER TABLE messages DROP COLUMN input_tokens;
ALTER TABLE messages DROP COLUMN generation_duration_ms;
@@ -0,0 +1,4 @@
ALTER TABLE messages ADD COLUMN generation_duration_ms INTEGER;
ALTER TABLE messages ADD COLUMN input_tokens INTEGER;
ALTER TABLE messages ADD COLUMN cached_tokens INTEGER;
ALTER TABLE messages ADD COLUMN output_tokens INTEGER;
@@ -0,0 +1 @@
ALTER TABLE messages DROP COLUMN model_content;
@@ -0,0 +1 @@
ALTER TABLE messages ADD COLUMN model_content TEXT;
@@ -0,0 +1 @@
ALTER TABLE sessions DROP COLUMN last_used;
@@ -0,0 +1,4 @@
ALTER TABLE sessions ADD COLUMN last_used BIGINT NOT NULL DEFAULT 0 CHECK (last_used >= 0);
UPDATE sessions
SET last_used = unixepoch();
@@ -0,0 +1 @@
ALTER TABLE sessions DROP COLUMN permission_mode;
@@ -0,0 +1,2 @@
ALTER TABLE sessions ADD COLUMN permission_mode TEXT NOT NULL DEFAULT 'heuristic'
CHECK (permission_mode IN ('heuristic', 'ai'));
@@ -0,0 +1 @@
ALTER TABLE messages DROP COLUMN tool_approval_reasons;
@@ -0,0 +1 @@
ALTER TABLE messages ADD COLUMN tool_approval_reasons TEXT;
@@ -0,0 +1 @@
ALTER TABLE messages DROP COLUMN instruction_metadata;
@@ -0,0 +1 @@
ALTER TABLE messages ADD COLUMN instruction_metadata TEXT;
@@ -0,0 +1 @@
ALTER TABLE sessions DROP COLUMN model;
@@ -0,0 +1,7 @@
ALTER TABLE sessions ADD COLUMN model TEXT
CHECK (model IS NULL OR model IN (
'deepseek-v4-flash-0731',
'deepseek-v4-pro',
'glm-5.2',
'glm-5.3-flash'
));
@@ -0,0 +1,13 @@
ALTER TABLE sessions ADD COLUMN model_without_qwen TEXT
CHECK (model_without_qwen IS NULL OR model_without_qwen IN (
'deepseek-v4-flash-0731',
'deepseek-v4-pro',
'glm-5.2',
'glm-5.3-flash'
));
UPDATE sessions SET model_without_qwen = CASE
WHEN model = 'qwen3.8-flash-next' THEN NULL
ELSE model
END;
ALTER TABLE sessions DROP COLUMN model;
ALTER TABLE sessions RENAME COLUMN model_without_qwen TO model;
@@ -0,0 +1,11 @@
ALTER TABLE sessions ADD COLUMN model_with_qwen TEXT
CHECK (model_with_qwen IS NULL OR model_with_qwen IN (
'deepseek-v4-flash-0731',
'deepseek-v4-pro',
'glm-5.2',
'glm-5.3-flash',
'qwen3.8-flash-next'
));
UPDATE sessions SET model_with_qwen = model;
ALTER TABLE sessions DROP COLUMN model;
ALTER TABLE sessions RENAME COLUMN model_with_qwen TO model;
+66
View File
@@ -0,0 +1,66 @@
#import <AppKit/AppKit.h>
#import <AVKit/AVKit.h>
#include <stdbool.h>
@interface DS4MediaWindowController : NSWindowController <NSWindowDelegate>
@end
static NSMutableSet<DS4MediaWindowController *> *DS4MediaWindows(void) {
static NSMutableSet<DS4MediaWindowController *> *windows;
static dispatch_once_t once;
dispatch_once(&once, ^{
windows = [NSMutableSet set];
});
return windows;
}
@implementation DS4MediaWindowController
- (void)windowWillClose:(NSNotification *)notification {
AVPlayerView *view = (AVPlayerView *)self.window.contentView;
[view.player pause];
[DS4MediaWindows() removeObject:self];
}
@end
bool ds4_media_open(const char *url_bytes, const char *title_bytes, bool video) {
if (url_bytes == NULL || title_bytes == NULL) {
return false;
}
NSString *url_string = [NSString stringWithUTF8String:url_bytes];
NSString *title = [NSString stringWithUTF8String:title_bytes];
NSURL *url = [NSURL URLWithString:url_string];
if (url == nil || title == nil) {
return false;
}
dispatch_async(dispatch_get_main_queue(), ^{
NSRect frame = NSMakeRect(0, 0, video ? 800 : 560, video ? 500 : 180);
NSWindow *window = [[NSWindow alloc]
initWithContentRect:frame
styleMask:NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable
backing:NSBackingStoreBuffered
defer:NO];
window.title = title.length > 0 ? title : url.lastPathComponent;
window.minSize = video ? NSMakeSize(480, 300) : NSMakeSize(420, 150);
AVPlayerView *view = [[AVPlayerView alloc] initWithFrame:frame];
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
view.player = [AVPlayer playerWithURL:url];
window.contentView = view;
DS4MediaWindowController *controller =
[[DS4MediaWindowController alloc] initWithWindow:window];
window.delegate = controller;
[DS4MediaWindows() addObject:controller];
[controller showWindow:nil];
[window center];
[window makeKeyAndOrderFront:nil];
[view.player play];
});
return true;
}
+5 -6
View File
@@ -1,15 +1,14 @@
# Vendored DS4 Metal boundary # Vendored DS4 Metal boundary
These files are a one-time snapshot of the DS4 Metal boundary from commit The Metal bridge and GPU declarations track the DS4 boundary audited at commit
`efdadd41e20134af4f3381e1ed90e96fe4faef6f`: `ec7642cdd9ec81d01ad4b1fd8f8a3d1511533748` (2026-08-31):
- `ds4_metal.m` - `ds4_metal.m`
- `ds4.h`
- `ds4_gpu.h` - `ds4_gpu.h`
- `ds4_ssd.h`
- `LICENSE`
The matching Metal kernels live in the repository-level `metal/` directory. The matching Metal kernels, including `glm53_*.metal`, live in the
repository-level `metal/` directory. The other headers remain DS4Server-owned
interfaces and are not copies of DS4 application code.
DS4Server builds and bundles this local snapshot; it does not read a sibling DS4Server builds and bundles this local snapshot; it does not read a sibling
DS4 checkout. Rust owns the model, graph, session, sampling, and lifecycle. DS4 checkout. Rust owns the model, graph, session, sampling, and lifecycle.
Objective-C remains only at the platform Metal boundary. Objective-C remains only at the platform Metal boundary.
+36 -5
View File
@@ -126,13 +126,10 @@ typedef struct {
typedef struct { typedef struct {
const char *model_path; const char *model_path;
const char *mtp_path;
ds4_backend backend; ds4_backend backend;
int n_threads; int n_threads;
int context_size; int context_size;
uint32_t prefill_chunk; uint32_t prefill_chunk;
int mtp_draft_tokens;
float mtp_margin;
float dspark_confidence_threshold; float dspark_confidence_threshold;
const char *directional_steering_file; const char *directional_steering_file;
const char *expert_profile_path; const char *expert_profile_path;
@@ -150,6 +147,7 @@ typedef struct {
bool glm_mtp_timing; bool glm_mtp_timing;
bool dspark; bool dspark;
bool dspark_strict; bool dspark_strict;
bool dspark_exact_sampling;
bool dspark_confidence_threshold_set; bool dspark_confidence_threshold_set;
bool cuda_tensor_parallel; bool cuda_tensor_parallel;
bool ssd_streaming; bool ssd_streaming;
@@ -158,6 +156,8 @@ typedef struct {
bool inspect_only; bool inspect_only;
/* Multi-GPU placement uses this to price per-layer KV storage. */ /* Multi-GPU placement uses this to price per-layer KV storage. */
int placement_ctx_hint; int placement_ctx_hint;
/* Number of independently allocated session graphs/caches to reserve. */
int placement_session_count_hint;
/* Server batch mode serializes execution and can share prefill scratch. */ /* Server batch mode serializes execution and can share prefill scratch. */
bool share_session_prefill_workspace; bool share_session_prefill_workspace;
bool first_token_test; bool first_token_test;
@@ -371,6 +371,30 @@ int ds4_test_sample_logits(const float *logits, uint32_t n_vocab,
float temperature, int top_k, float temperature, int top_k,
float top_p, float min_p, uint64_t *rng, float top_p, float min_p, uint64_t *rng,
float *prob_scratch); float *prob_scratch);
int ds4_test_sampling_probabilities(const float *logits, uint32_t n_vocab,
float temperature, int top_k,
float top_p, float min_p, float *probs);
int ds4_test_speculative_sample(const float *target_logits,
const float *draft_logits,
uint32_t n_vocab,
float temperature,
int top_k,
float top_p,
float min_p,
uint64_t *rng,
float *target_probs,
float *draft_probs);
int ds4_test_speculative_delta_sample(const float *target_logits,
uint32_t n_vocab,
int draft_token,
float temperature,
int top_k,
float top_p,
float min_p,
uint64_t *rng,
float *target_probs);
int ds4_test_argmax_excluding_logits(const float *logits, uint32_t n_vocab,
int excluded_id);
uint64_t ds4_test_mixed_native_count(void); uint64_t ds4_test_mixed_native_count(void);
#endif #endif
int ds4_session_top_logprobs(ds4_session *s, ds4_token_score *out, int k); int ds4_session_top_logprobs(ds4_session *s, ds4_token_score *out, int k);
@@ -403,6 +427,15 @@ int ds4_session_eval_speculative_argmax(ds4_session *s, int first_token,
int max_tokens, int eos_token, int max_tokens, int eos_token,
int *accepted, int accepted_cap, int *accepted, int accepted_cap,
char *err, size_t errlen); char *err, size_t errlen);
/* Evaluate one already-sampled target token and speculatively extend it.
* Positive-temperature DSpark normally commits greedily verified draft
* tokens; dspark_exact_sampling selects exact stochastic p/q acceptance. */
int ds4_session_eval_speculative(ds4_session *s, int first_token,
int max_tokens, int eos_token,
float temperature, int top_k,
float top_p, float min_p, uint64_t *rng,
int *accepted, int accepted_cap,
char *err, size_t errlen);
/* TP worker side of a mirrored speculative-verify block: run its half of the /* TP worker side of a mirrored speculative-verify block: run its half of the
* batch verify for KV side effects, then obey the leader's commit frame * batch verify for KV side effects, then obey the leader's commit frame
* (keep, or roll back and replay). Only called from ds4_tp_worker_run. */ * (keep, or roll back and replay). Only called from ds4_tp_worker_run. */
@@ -415,8 +448,6 @@ int ds4_session_ctx(ds4_session *s);
int ds4_session_prefill_cap(ds4_session *s); int ds4_session_prefill_cap(ds4_session *s);
int ds4_engine_routed_quant_bits(ds4_engine *e); int ds4_engine_routed_quant_bits(ds4_engine *e);
bool ds4_engine_has_output_head(ds4_engine *e); bool ds4_engine_has_output_head(ds4_engine *e);
bool ds4_engine_has_mtp(ds4_engine *e);
int ds4_engine_mtp_draft_tokens(ds4_engine *e);
const ds4_tokens *ds4_session_tokens(ds4_session *s); const ds4_tokens *ds4_session_tokens(ds4_session *s);
/* Low-level graph slice entry points used by distributed inference. The /* Low-level graph slice entry points used by distributed inference. The
+66
View File
@@ -0,0 +1,66 @@
#import <Foundation/Foundation.h>
#import <Metal/Metal.h>
#include <mach/mach_time.h>
#include <time.h>
#include "ds4_gpu.h"
/* Shared optional UI/headless probe. Process placement matters: a separate
* queue in the model process is not a separate-process scheduling test. */
static id<MTLDevice> g_canary_device;
static id<MTLCommandQueue> g_canary_queue;
static id<MTLBuffer> g_canary_buffer;
static double ds4_monotonic_seconds(void) {
struct timespec time;
if (clock_gettime(CLOCK_MONOTONIC, &time) != 0) return 0.0;
return (double)time.tv_sec + (double)time.tv_nsec / 1000000000.0;
}
int ds4_gpu_canary_probe(ds4_gpu_canary_sample *sample) {
if (!sample) return 0;
sample->scheduled_seconds = 0.0;
sample->completed_seconds = 0.0;
sample->gpu_wait_seconds = -1.0;
sample->gpu_interval_seconds = -1.0;
sample->host_return_seconds = -1.0;
@autoreleasepool {
if (!g_canary_device) g_canary_device = MTLCreateSystemDefaultDevice();
if (!g_canary_queue && g_canary_device) {
g_canary_queue = [g_canary_device newCommandQueue];
}
if (!g_canary_buffer && g_canary_device) {
g_canary_buffer = [g_canary_device newBufferWithLength:4096
options:MTLResourceStorageModeShared];
}
if (!g_canary_queue || !g_canary_buffer) return 0;
id<MTLCommandBuffer> cb = [g_canary_queue commandBuffer];
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
if (!cb || !blit) return 0;
[blit fillBuffer:g_canary_buffer range:NSMakeRange(0, 4096) value:0];
[blit endEncoding];
mach_timebase_info_data_t timebase;
if (mach_timebase_info(&timebase) != KERN_SUCCESS || !timebase.denom) return 0;
const double scale = (double)timebase.numer / timebase.denom / 1e9;
const double started = ds4_monotonic_seconds();
const double mach_started = (double)mach_absolute_time() * scale;
[cb commit];
[cb waitUntilScheduled];
sample->scheduled_seconds = ds4_monotonic_seconds() - started;
[cb waitUntilCompleted];
const double mach_returned = (double)mach_absolute_time() * scale;
sample->completed_seconds = ds4_monotonic_seconds() - started;
// Metal GPU times use system mach time, unlike CLOCK_MONOTONIC on
// macOS. Read only after completion; unavailable timestamps stay -1.
const double gpu_start = cb.GPUStartTime;
const double gpu_end = cb.GPUEndTime;
if (gpu_start > 0.0 && gpu_start >= mach_started &&
gpu_end >= gpu_start && mach_returned >= gpu_end) {
sample->gpu_wait_seconds = gpu_start - mach_started;
// Includes GPU scheduling/preemption, not exclusive busy time.
sample->gpu_interval_seconds = gpu_end - gpu_start;
sample->host_return_seconds = mach_returned - gpu_end;
}
return cb.status == MTLCommandBufferStatusCompleted;
}
}
+666 -13
View File
@@ -43,12 +43,40 @@ typedef struct {
int ds4_gpu_init(void); int ds4_gpu_init(void);
void ds4_gpu_cleanup(void); void ds4_gpu_cleanup(void);
int ds4_gpu_metal4_tensor_api_enabled(void);
ds4_gpu_tensor *ds4_gpu_tensor_alloc(uint64_t bytes); ds4_gpu_tensor *ds4_gpu_tensor_alloc(uint64_t bytes);
ds4_gpu_tensor *ds4_gpu_tensor_alloc_untracked(uint64_t bytes);
void *ds4_gpu_mtplx_heap_create(uint64_t bytes);
void ds4_gpu_mtplx_heap_free(void *heap);
ds4_gpu_tensor *ds4_gpu_mtplx_tensor_alloc(uint64_t bytes, void *heap);
ds4_gpu_tensor *ds4_gpu_mtplx_tensor_empty(void);
int ds4_gpu_tensor_has_heap(const ds4_gpu_tensor *tensor);
int ds4_gpu_mtplx_residency_supported(void);
void *ds4_gpu_mtplx_residency_create(void);
void ds4_gpu_mtplx_residency_free(void *set);
void ds4_gpu_mtplx_residency_allocation(void *set, void *allocation, int add);
void ds4_gpu_mtplx_residency_commit(void *set);
void ds4_gpu_mtplx_residency_attach(void *queue, void *const *sets, uint64_t count);
void *ds4_gpu_mtplx_command_queue(void);
void *ds4_gpu_mtplx_retain_command_queue(void);
void ds4_gpu_mtplx_queue_free(void *queue);
int ds4_gpu_mtplx_submission_hook(void *context, void (*before_commit)(void *, void *));
void *ds4_gpu_mtplx_tensor_allocation(const ds4_gpu_tensor *tensor);
uint64_t ds4_gpu_mtplx_allocated_size(void *allocation);
uint64_t ds4_gpu_mtplx_residency_count(void *set);
int ds4_gpu_mtplx_residency_contains(void *set, void *allocation);
ds4_gpu_tensor *ds4_gpu_tensor_alloc_managed(uint64_t bytes); ds4_gpu_tensor *ds4_gpu_tensor_alloc_managed(uint64_t bytes);
ds4_gpu_tensor *ds4_gpu_tensor_view(const ds4_gpu_tensor *base, uint64_t offset, uint64_t bytes); ds4_gpu_tensor *ds4_gpu_tensor_view(const ds4_gpu_tensor *base, uint64_t offset, uint64_t bytes);
void ds4_gpu_tensor_free(ds4_gpu_tensor *tensor); void ds4_gpu_tensor_free(ds4_gpu_tensor *tensor);
/* Transfer context to a completion callback without committing or waiting.
* Releases immediately when there is no open or pending work. An empty open
* batch returns 0 without taking ownership; callers must retain the context. */
int ds4_gpu_defer_until_completed(void *context, void (*release)(void *));
uint64_t ds4_gpu_max_buffer_length(void);
uint64_t ds4_gpu_tensor_bytes(const ds4_gpu_tensor *tensor); uint64_t ds4_gpu_tensor_bytes(const ds4_gpu_tensor *tensor);
/* Borrowed CPU pointer, valid while the caller owns the tensor. This does not
* retain storage beyond that ownership or synchronize pending GPU work. */
void *ds4_gpu_tensor_contents(ds4_gpu_tensor *tensor); void *ds4_gpu_tensor_contents(ds4_gpu_tensor *tensor);
int ds4_gpu_tensor_fill_f32(ds4_gpu_tensor *tensor, float value, uint64_t count); int ds4_gpu_tensor_fill_f32(ds4_gpu_tensor *tensor, float value, uint64_t count);
int ds4_gpu_tensor_write(ds4_gpu_tensor *tensor, uint64_t offset, const void *data, uint64_t bytes); int ds4_gpu_tensor_write(ds4_gpu_tensor *tensor, uint64_t offset, const void *data, uint64_t bytes);
@@ -78,6 +106,39 @@ int ds4_gpu_begin_commands(void);
int ds4_gpu_flush_encoder(void); int ds4_gpu_flush_encoder(void);
int ds4_gpu_flush_commands(void); int ds4_gpu_flush_commands(void);
int ds4_gpu_commands_active(void); int ds4_gpu_commands_active(void);
typedef struct {
double busy_seconds;
double max_busy_seconds;
uint64_t command_buffers;
} ds4_gpu_busy_stats;
void ds4_gpu_busy_stats_reset(void);
void ds4_gpu_busy_stats_get(ds4_gpu_busy_stats *stats);
typedef struct {
double scheduled_seconds;
double completed_seconds;
double gpu_wait_seconds;
double gpu_interval_seconds;
double host_return_seconds;
} ds4_gpu_canary_sample;
int ds4_gpu_canary_probe(ds4_gpu_canary_sample *sample);
#ifdef __APPLE__
int ds4_gpu_parallel_ffn_finish(void);
void ds4_gpu_parallel_ffn_abort(void);
int ds4_gpu_parallel_ffn_start(
ds4_gpu_tensor *gate,
ds4_gpu_tensor *up,
ds4_gpu_tensor *mid,
ds4_gpu_tensor *shared_out,
const void *model_map,
uint64_t model_size,
uint64_t gate_offset,
uint64_t up_offset,
uint64_t down_offset,
uint32_t model_dim,
uint32_t shared_dim,
const ds4_gpu_tensor *x,
float clamp);
#endif
int ds4_gpu_signal_selected_readback_ready(uint64_t *event_value); int ds4_gpu_signal_selected_readback_ready(uint64_t *event_value);
int ds4_gpu_commit_and_wait_selected_readback(uint64_t event_value, const char *label); int ds4_gpu_commit_and_wait_selected_readback(uint64_t event_value, const char *label);
int ds4_gpu_wait_selected_readback_ready(uint64_t event_value, const char *label); int ds4_gpu_wait_selected_readback_ready(uint64_t event_value, const char *label);
@@ -95,8 +156,107 @@ int ds4_gpu_synchronize(void);
int ds4_gpu_set_model_map(const void *model_map, uint64_t model_size); int ds4_gpu_set_model_map(const void *model_map, uint64_t model_size);
int ds4_gpu_set_model_fd(int fd); int ds4_gpu_set_model_fd(int fd);
int ds4_gpu_set_model_fd_for_map(int fd, const void *model_map); int ds4_gpu_set_model_fd_for_map(int fd, const void *model_map);
int ds4_gpu_build_derived_artifacts(const void *model_map, uint64_t model_size,
const char *model_path);
int ds4_gpu_model_range_replaced(const void *model_map, uint64_t offset,
uint64_t bytes);
int ds4_gpu_set_model_map_range(const void *model_map, uint64_t model_size, uint64_t map_offset, uint64_t map_size, uint64_t max_tensor_bytes); int ds4_gpu_set_model_map_range(const void *model_map, uint64_t model_size, uint64_t map_offset, uint64_t map_size, uint64_t max_tensor_bytes);
int ds4_gpu_set_transient_model_map_range(const void *model_map, uint64_t model_size, uint64_t map_offset, uint64_t map_size, uint64_t max_tensor_bytes);
/* Caller must finish every command that references this mapping first. */
int ds4_gpu_release_transient_model_map(const void *model_map, uint64_t model_size);
int ds4_gpu_model_map_active(const void *model_map, uint64_t model_size);
/* Add a secondary GGUF mapping without replacing the primary model mapping. */
int ds4_gpu_set_aux_model_map_range(const void *model_map,
uint64_t model_size,
uint64_t map_offset,
uint64_t map_size);
int ds4_gpu_set_model_map_spans(const void *model_map, uint64_t model_size, const uint64_t *offsets, const uint64_t *sizes, uint32_t count, uint64_t max_tensor_bytes); int ds4_gpu_set_model_map_spans(const void *model_map, uint64_t model_size, const uint64_t *offsets, const uint64_t *sizes, uint32_t count, uint64_t max_tensor_bytes);
typedef struct {
const ds4_gpu_tensor *tensor;
const void *map;
uint64_t size;
uint64_t offset;
uint64_t bytes;
} ds4_gpu_qwen_weight_view;
typedef struct {
uint32_t u[16];
float f[8];
} ds4_gpu_qwen_kernel_args;
int ds4_gpu_qwen_dispatch(
const char *kernel,
ds4_gpu_tensor *out,
const ds4_gpu_tensor *a,
const ds4_gpu_tensor *b,
const ds4_gpu_tensor *c,
const ds4_gpu_tensor *d,
const ds4_gpu_qwen_weight_view *weights,
uint32_t weight_count,
const ds4_gpu_qwen_kernel_args *args,
uint32_t grid_x,
uint32_t grid_y);
/* Ordered contiguous buffers: reference inputs first, then outputs. No dtype
* conversion, argument remapping, extra flush, or inferred thread geometry. */
int ds4_gpu_qwen_dispatch_buffers(
const char *kernel,
const ds4_gpu_tensor *const *buffers,
uint32_t count,
const uint32_t grid[3],
const uint32_t threadgroup[3]);
/* Native Metal bindings only; Rust selects kernels, parameters and geometry. */
typedef struct {
const ds4_gpu_tensor *tensor;
const void *bytes;
uint32_t size;
uint32_t index;
uint64_t offset;
} ds4_gpu_metal_binding;
typedef struct {
uint32_t index;
uint32_t value;
uint32_t kind; /* 0: Bool, 1: I32 bit pattern. */
} ds4_gpu_metal_constant;
/* Optional Rust allocation owner: clear *resource_owner only when transferred
* to the command completion handler. On earlier failure the caller retains it. */
int ds4_gpu_mtplx_dispatch(
const char *kernel,
const ds4_gpu_metal_binding *bindings, uint32_t count,
const ds4_gpu_metal_constant *constants, uint32_t constant_count,
const uint32_t groups[3], const uint32_t threads[3], uint32_t individual_threads,
void **resource_owner, void (*release_owner)(void *));
/* Same dispatch, on a Rust-owned command buffer/concurrent encoder. Null
* handles select the existing dispatch path. No implicit end/commit/wait. */
int ds4_gpu_mtplx_encode(
const char *kernel,
const ds4_gpu_metal_binding *bindings, uint32_t count,
const ds4_gpu_metal_constant *constants, uint32_t constant_count,
const uint32_t groups[3], const uint32_t threads[3], uint32_t individual_threads,
void **resource_owner, void (*release_owner)(void *),
void *command_buffer, void *encoder, uint32_t barrier);
void *ds4_gpu_mtplx_queue_create(void);
void *ds4_gpu_mtplx_command_create(void *queue);
void *ds4_gpu_mtplx_encoder_create(void *command);
void *ds4_gpu_mtplx_fence_create(void);
void ds4_gpu_mtplx_object_free(void *object);
void ds4_gpu_mtplx_encoder_barrier(void *encoder);
void ds4_gpu_mtplx_encoder_fence(void *encoder, void *fence, int update);
void ds4_gpu_mtplx_encoder_end(void *encoder);
void ds4_gpu_mtplx_command_completed(void *command, void *context,
void (*completed)(void *, const char *));
void ds4_gpu_mtplx_command_commit(void *command);
void ds4_gpu_mtplx_command_wait(void *command);
uint32_t ds4_gpu_mtplx_arch_suffix(void);
void ds4_gpu_mtplx_nax_platform(uint32_t out[4]);
void *ds4_gpu_mtplx_event_create(void);
void ds4_gpu_mtplx_command_event(void *command, void *event, uint64_t value, int signal);
void ds4_gpu_mtplx_event_signal(void *event, uint64_t value);
void ds4_gpu_mtplx_event_wait(void *event, uint64_t value);
uint64_t ds4_gpu_mtplx_event_value(void *event);
/* With source == NULL, only query the existing pipeline cache. */
int ds4_gpu_mtplx_compile(const char *kernel, const char *source);
uint32_t ds4_gpu_mtplx_kernel_max_threads(const char *kernel);
int ds4_gpu_cache_model_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, const char *label); int ds4_gpu_cache_model_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, const char *label);
int ds4_gpu_cache_q8_f16_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, uint64_t in_dim, uint64_t out_dim, const char *label); int ds4_gpu_cache_q8_f16_range(const void *model_map, uint64_t model_size, uint64_t offset, uint64_t bytes, uint64_t in_dim, uint64_t out_dim, const char *label);
int ds4_gpu_q8_cache_suppressed(void); int ds4_gpu_q8_cache_suppressed(void);
@@ -138,16 +298,57 @@ int ds4_gpu_preload_q4_expert_tables(const void *model_map, uint64_t model_size,
int ds4_gpu_should_use_managed_kv_cache(uint64_t kv_cache_bytes, uint64_t context_bytes); int ds4_gpu_should_use_managed_kv_cache(uint64_t kv_cache_bytes, uint64_t context_bytes);
void ds4_gpu_set_quality(bool quality); void ds4_gpu_set_quality(bool quality);
void ds4_gpu_set_glm_model(bool enabled); void ds4_gpu_set_glm_model(bool enabled);
void ds4_gpu_set_qwen_model(bool enabled);
void ds4_gpu_set_ssd_streaming(bool enabled); void ds4_gpu_set_ssd_streaming(bool enabled);
void ds4_gpu_set_glm_streaming_prefill_full_layer(bool enabled); void ds4_gpu_set_glm_streaming_prefill_full_layer(bool enabled);
#ifdef __APPLE__ #ifdef __APPLE__
int ds4_gpu_device_is_pre_m5_apple_silicon(void);
int ds4_gpu_device_is_m5_apple_silicon(void);
int ds4_gpu_set_decode_pipeline_fast_lookup(int enabled);
/* Strict test oracle for the fixed decode mul_mv pipeline lookup cache. */
int ds4_gpu_test_decode_pipeline_fast_lookup(void);
/* Strict test oracle for the extended decode mul_mv_ext (nsg + nxpsg) cache. */
int ds4_gpu_test_decode_pipeline_fast_lookup_ext(void);
/* Strict test oracle for the generated resident-prefill MXFP4 half LUT. */
int ds4_gpu_test_mxfp4_down_half_lut(uint16_t *legacy_bits,
uint16_t *lut_bits);
enum {
DS4_GPU_TEST_MXFP4_PAIR_TAIL_CULL = 1u << 0,
DS4_GPU_TEST_MXFP4_PAIR_COMPACT_TILE = 1u << 1,
DS4_GPU_TEST_MXFP4_MAP_SCATTER = 1u << 2,
DS4_GPU_TEST_MXFP4_DOWN_TAIL_CULL = 1u << 3,
DS4_GPU_TEST_MXFP4_DOWN_HALF_LUT = 1u << 4,
DS4_GPU_TEST_OUTPUT_HC_WEIGHTS4 = 1u << 5,
DS4_GPU_TEST_HC_RMS_SCALE_PROJ = 1u << 6,
};
void ds4_gpu_test_set_flags(uint32_t flags);
void ds4_gpu_release_zero_prefix_prefill_mask_cache(void); void ds4_gpu_release_zero_prefix_prefill_mask_cache(void);
#else
static inline int ds4_gpu_device_is_pre_m5_apple_silicon(void) { return 0; }
static inline int ds4_gpu_device_is_m5_apple_silicon(void) { return 0; }
#endif #endif
void ds4_gpu_set_streaming_expert_cache_budget(uint32_t experts); void ds4_gpu_set_streaming_expert_cache_budget(uint32_t experts);
void ds4_gpu_set_streaming_expert_cache_expert_bytes(uint64_t bytes); void ds4_gpu_set_streaming_expert_cache_expert_bytes(uint64_t bytes);
uint64_t ds4_gpu_recommended_working_set_size(void); uint64_t ds4_gpu_recommended_working_set_size(void);
uint64_t ds4_gpu_current_allocated_size(void);
uint32_t ds4_gpu_stream_expert_cache_configured_count(void); uint32_t ds4_gpu_stream_expert_cache_configured_count(void);
uint32_t ds4_gpu_stream_expert_cache_current_count(void); uint32_t ds4_gpu_stream_expert_cache_current_count(void);
typedef struct ds4_gpu_stream_expert_cache_stats {
uint32_t configured_count;
uint32_t current_count;
uint64_t hits;
uint64_t misses;
uint64_t evictions;
uint64_t wraps;
uint64_t buffer_allocs;
uint64_t buffer_reuses;
uint64_t evict_advise_bytes;
uint64_t willneed_advise_bytes;
uint64_t pread_bytes;
double pread_ms;
} ds4_gpu_stream_expert_cache_stats;
void ds4_gpu_stream_expert_cache_get_stats(
ds4_gpu_stream_expert_cache_stats *stats);
typedef struct ds4_gpu_stream_expert_table { typedef struct ds4_gpu_stream_expert_table {
const void *model_map; const void *model_map;
uint64_t model_size; uint64_t model_size;
@@ -200,6 +401,7 @@ int ds4_gpu_stream_expert_cache_seed_from_layer_selected(
uint32_t n_tokens, uint32_t n_tokens,
uint32_t n_seed_tokens, uint32_t n_seed_tokens,
uint32_t n_selected); uint32_t n_selected);
int ds4_gpu_stream_expert_cache_finish_pending_batch(void);
int ds4_gpu_stream_expert_cache_release_layer_cache(void); int ds4_gpu_stream_expert_cache_release_layer_cache(void);
#endif #endif
int ds4_gpu_stream_expert_cache_seed_experts( int ds4_gpu_stream_expert_cache_seed_experts(
@@ -207,6 +409,14 @@ int ds4_gpu_stream_expert_cache_seed_experts(
const int32_t *expert_ids, const int32_t *expert_ids,
const uint32_t *expert_priorities, const uint32_t *expert_priorities,
uint32_t n_experts); uint32_t n_experts);
#ifdef __APPLE__
/* Seed from mapped weights with blits appended to the active command buffer. */
int ds4_gpu_stream_expert_cache_seed_experts_gpu_copy(
const ds4_gpu_stream_expert_table *table,
const int32_t *expert_ids,
const uint32_t *expert_priorities,
uint32_t n_experts);
#endif
void ds4_gpu_print_memory_report(const char *label); void ds4_gpu_print_memory_report(const char *label);
/* Tensor-parallel per-layer gates (Metal only). The encoder calls /* Tensor-parallel per-layer gates (Metal only). The encoder calls
@@ -598,6 +808,17 @@ int ds4_gpu_matmul_q8_0_pair_tensor(
const ds4_gpu_tensor *x, const ds4_gpu_tensor *x,
uint64_t n_tok); uint64_t n_tok);
int ds4_gpu_matmul_q4_K_pair_decode_tensor(
ds4_gpu_tensor *out0,
ds4_gpu_tensor *out1,
const void *model_map,
uint64_t model_size,
uint64_t weight0_offset,
uint64_t weight1_offset,
uint64_t in_dim,
uint64_t out_dim,
const ds4_gpu_tensor *x);
/* Multi-row decode projections that preserve the one-row reduction order. */ /* Multi-row decode projections that preserve the one-row reduction order. */
int ds4_gpu_matmul_q8_0_decode_rows_exact_tensor( int ds4_gpu_matmul_q8_0_decode_rows_exact_tensor(
ds4_gpu_tensor *out, ds4_gpu_tensor *out,
@@ -643,6 +864,36 @@ int ds4_gpu_shared_gate_up_swiglu_q8_0_tensor(
uint64_t out_dim, uint64_t out_dim,
const ds4_gpu_tensor *x, const ds4_gpu_tensor *x,
float clamp); float clamp);
int ds4_gpu_router_shared_gate_up_q8_0_tensor(
ds4_gpu_tensor *router_logits,
ds4_gpu_tensor *gate,
ds4_gpu_tensor *up,
ds4_gpu_tensor *mid,
const void *model_map,
uint64_t model_size,
uint64_t router_weight_offset,
uint64_t gate_offset,
uint64_t up_offset,
uint64_t in_dim,
uint64_t router_out_dim,
uint64_t out_dim,
const ds4_gpu_tensor *x,
float clamp,
bool router_only);
#ifdef __APPLE__
int ds4_gpu_router_project_select_fused_tensor(
ds4_gpu_tensor *router_logits,
ds4_gpu_tensor *probs,
ds4_gpu_tensor *selected,
ds4_gpu_tensor *weights,
const void *model_map,
uint64_t model_size,
uint64_t router_weight_offset,
uint64_t bias_offset,
bool has_bias,
const ds4_gpu_tensor *x);
#endif
int ds4_gpu_shared_mid_swiglu_q8_0_decode_exact_tensor( int ds4_gpu_shared_mid_swiglu_q8_0_decode_exact_tensor(
ds4_gpu_tensor *mid, ds4_gpu_tensor *mid,
const void *model_map, const void *model_map,
@@ -720,6 +971,20 @@ int ds4_gpu_matmul_f16_tensor(
const ds4_gpu_tensor *x, const ds4_gpu_tensor *x,
uint64_t n_tok); uint64_t n_tok);
/* CUDA batch path: fold an input RMS normalization into the FP16 activation
* conversion used by the following projection. Returns 0 without touching
* out when the optimized path is unavailable. */
int ds4_gpu_matmul_f16_rms_fold_tensor(
ds4_gpu_tensor *out,
const void *model_map,
uint64_t model_size,
uint64_t weight_offset,
uint64_t in_dim,
uint64_t out_dim,
const ds4_gpu_tensor *x,
uint64_t n_tok,
float norm_eps);
/* Exact multi-row form of the DeepSeek 4096x256 F16 router projection. */ /* Exact multi-row form of the DeepSeek 4096x256 F16 router projection. */
int ds4_gpu_matmul_f16_router_rows_exact_tensor( int ds4_gpu_matmul_f16_router_rows_exact_tensor(
ds4_gpu_tensor *out, ds4_gpu_tensor *out,
@@ -761,6 +1026,95 @@ int ds4_gpu_matmul_f16_pair_compressor_store_tensor(
uint32_t ratio, uint32_t ratio,
uint32_t pos); uint32_t pos);
int ds4_gpu_matmul_f16_quad_compressor_store_tensor(
ds4_gpu_tensor *out0_kv,
ds4_gpu_tensor *out0_score,
ds4_gpu_tensor *out1_kv,
ds4_gpu_tensor *out1_score,
ds4_gpu_tensor *state0_kv,
ds4_gpu_tensor *state0_score,
ds4_gpu_tensor *state1_kv,
ds4_gpu_tensor *state1_score,
const void *model_map,
uint64_t model_size,
uint64_t weight0_kv_offset,
uint64_t weight0_score_offset,
uint64_t weight1_kv_offset,
uint64_t weight1_score_offset,
uint64_t ape0_offset,
uint32_t ape0_type,
uint64_t ape1_offset,
uint32_t ape1_type,
uint64_t in_dim,
uint32_t width0,
uint32_t width1,
const ds4_gpu_tensor *x,
uint32_t ratio,
uint32_t pos);
/* Decode-only M5 fusion: emit-path compressor row finalize (norm + rope +
* fp8/commit + indexer qat) in one dispatch. Bit-exact vs the separate
* dispatches. Returns 1 when fused, 0 to fall back. */
int ds4_gpu_dsv4_comp_row_finalize_tensor(
ds4_gpu_tensor *attn_stage,
ds4_gpu_tensor *attn_cache,
uint32_t attn_comp_row,
uint64_t attn_norm_offset,
ds4_gpu_tensor *index_cache,
uint32_t index_comp_row,
uint64_t index_norm_offset,
ds4_gpu_tensor *attn_state_kv,
ds4_gpu_tensor *attn_state_score,
ds4_gpu_tensor *index_state_kv,
ds4_gpu_tensor *index_state_score,
const void *model_map,
uint64_t model_size,
uint32_t pos,
uint32_t n_rot,
uint32_t n_ctx_orig,
float freq_base,
float freq_scale,
float ext_factor,
float attn_factor,
float beta_fast,
float beta_slow,
float rms_eps);
/* Decode-only M5 fusion: q_a/kv Q8 pair projection + F16 quad compressor
* projection/store in one dispatch. Bit-exact vs the separate dispatches.
* Returns 1 when fused, 0 to fall back, -1 on error. */
int ds4_gpu_qkv_pair_quad_compressor_store_tensor(
ds4_gpu_tensor *qr,
ds4_gpu_tensor *kv_raw,
ds4_gpu_tensor *out0_kv,
ds4_gpu_tensor *out0_score,
ds4_gpu_tensor *out1_kv,
ds4_gpu_tensor *out1_score,
ds4_gpu_tensor *state0_kv,
ds4_gpu_tensor *state0_score,
ds4_gpu_tensor *state1_kv,
ds4_gpu_tensor *state1_score,
const void *model_map,
uint64_t model_size,
uint64_t q_a_offset,
uint64_t kv_offset,
uint64_t weight0_kv_offset,
uint64_t weight0_score_offset,
uint64_t weight1_kv_offset,
uint64_t weight1_score_offset,
uint64_t ape0_offset,
uint32_t ape0_type,
uint64_t ape1_offset,
uint32_t ape1_type,
uint32_t in_dim,
uint32_t q_rank,
uint32_t kv_dim,
uint32_t width0,
uint32_t width1,
const ds4_gpu_tensor *x,
uint32_t ratio,
uint32_t pos);
int ds4_gpu_matmul_f32_tensor( int ds4_gpu_matmul_f32_tensor(
ds4_gpu_tensor *out, ds4_gpu_tensor *out,
const void *model_map, const void *model_map,
@@ -841,6 +1195,31 @@ int ds4_gpu_dsv4_qkv_rms_norm_rows_tensor(
uint32_t rows, uint32_t rows,
float eps); float eps);
int ds4_gpu_dsv4_qkv_rms_norm_kv_rope_fp8_store_tensor(
ds4_gpu_tensor *q_out,
const ds4_gpu_tensor *q,
const void *model_map,
uint64_t model_size,
uint64_t q_weight_offset,
uint32_t q_n,
ds4_gpu_tensor *kv_out,
const ds4_gpu_tensor *kv,
uint64_t kv_weight_offset,
uint32_t kv_n,
ds4_gpu_tensor *raw_cache,
uint64_t raw_cap,
uint32_t raw_row,
uint32_t n_rot,
uint32_t pos0,
uint32_t n_ctx_orig,
float freq_base,
float freq_scale,
float ext_factor,
float attn_factor,
float beta_fast,
float beta_slow,
float eps);
int ds4_gpu_dsv4_qkv_rms_norm_rows_kv_rope_tensor( int ds4_gpu_dsv4_qkv_rms_norm_rows_kv_rope_tensor(
ds4_gpu_tensor *q_out, ds4_gpu_tensor *q_out,
const ds4_gpu_tensor *q, const ds4_gpu_tensor *q,
@@ -926,6 +1305,8 @@ int ds4_gpu_dsv4_indexer_qat_tensor(
uint32_t n_rows, uint32_t n_rows,
uint32_t head_dim); uint32_t head_dim);
int ds4_gpu_rope_tail_tensor( int ds4_gpu_rope_tail_tensor(
ds4_gpu_tensor *x, ds4_gpu_tensor *x,
uint32_t n_tok, uint32_t n_tok,
@@ -1046,6 +1427,37 @@ int ds4_gpu_glm_store_indexer_k_tensor(
float beta_slow, float beta_slow,
bool cache_f16); bool cache_f16);
/* GLM-5.3 pools four normalized indexer keys with a learned, per-channel
* softmax. Partial pools are retained in tail_k/tail_gate across calls. */
int ds4_gpu_glm53_indexer_pool_update_tensor(
ds4_gpu_tensor *pool_cache,
ds4_gpu_tensor *tail_k,
ds4_gpu_tensor *tail_gate,
const ds4_gpu_tensor *raw_k,
const ds4_gpu_tensor *gate,
const void *model_map,
uint64_t model_size,
uint64_t norm_weight_offset,
uint64_t norm_bias_offset,
uint64_t ape_offset,
uint32_t pos0,
uint32_t n_tokens,
uint32_t cache_cap,
uint32_t head_dim,
uint32_t pool_size,
float eps,
bool cache_f16);
int ds4_gpu_glm53_expand_pool_selection_tensor(
ds4_gpu_tensor *raw_selected,
const ds4_gpu_tensor *pool_selected,
uint32_t n_tokens,
uint32_t pos0,
uint32_t selected_pools,
uint32_t index_topk,
uint32_t pool_size,
uint32_t output_width);
int ds4_gpu_glm_build_kv_cache_tensor( int ds4_gpu_glm_build_kv_cache_tensor(
ds4_gpu_tensor *key_cache, ds4_gpu_tensor *key_cache,
ds4_gpu_tensor *value_cache, ds4_gpu_tensor *value_cache,
@@ -1158,6 +1570,20 @@ int ds4_gpu_glm_indexer_scores_batch_tensor(
float scale, float scale,
bool cache_f16); bool cache_f16);
int ds4_gpu_glm53_indexer_scores_batch_tensor(
ds4_gpu_tensor *scores,
const ds4_gpu_tensor *q,
const ds4_gpu_tensor *weights,
const ds4_gpu_tensor *indexer_key_cache,
uint32_t n_rows,
uint32_t n_tokens,
uint32_t pos0,
uint32_t pool_size,
uint32_t n_head,
uint32_t head_dim,
float scale,
bool cache_f16);
int ds4_gpu_glm_qk_lowrank_q8_0_tensor( int ds4_gpu_glm_qk_lowrank_q8_0_tensor(
ds4_gpu_tensor *qk_low, ds4_gpu_tensor *qk_low,
const ds4_gpu_tensor *q, const ds4_gpu_tensor *q,
@@ -1468,6 +1894,21 @@ int ds4_gpu_glm_attention_indexed_batch_lora_causal_tensor(
float beta_fast, float beta_fast,
float beta_slow); float beta_slow);
/* Dense causal MLA over the shared compact latent cache. qk_low and lora_out
* are [token, head, kv_lora_dim]; the F16 cache is shared by all heads. */
int ds4_gpu_glm_attention_dense_compact_lora_causal_tensor(
ds4_gpu_tensor *lora_out,
const ds4_gpu_tensor *qk_low,
const ds4_gpu_tensor *kv_lora_cache,
uint32_t q_row0,
uint32_t n_q,
uint32_t n_kv,
uint32_t cache_cap,
bool cache_f16,
uint32_t n_head,
uint32_t kv_lora_dim,
uint32_t qk_dim);
int ds4_gpu_glm_attention_indexed_batch_lora_valid_tensor( int ds4_gpu_glm_attention_indexed_batch_lora_valid_tensor(
ds4_gpu_tensor *lora_out, ds4_gpu_tensor *lora_out,
const ds4_gpu_tensor *q, const ds4_gpu_tensor *q,
@@ -1519,6 +1960,19 @@ int ds4_gpu_glm_attention_flash_tensor(
uint32_t value_dim, uint32_t value_dim,
bool cache_f16); bool cache_f16);
/* Qwen prefill attention over already staged head-major F16 K/V tensors. */
int ds4_gpu_qwen_attention_flash_staged_tensor(
ds4_gpu_tensor *heads,
const ds4_gpu_tensor *q,
const ds4_gpu_tensor *key_cache,
const ds4_gpu_tensor *value_cache,
uint32_t pos0,
uint32_t n_tokens,
uint32_t cache_len,
uint32_t n_head,
uint32_t n_kv_head,
uint32_t head_dim);
/* Release decode fused KV finalizer: after the standalone RoPE kernel, this /* Release decode fused KV finalizer: after the standalone RoPE kernel, this
* performs DS4's FP8 non-RoPE KV round trip and writes the F16-rounded raw * performs DS4's FP8 non-RoPE KV round trip and writes the F16-rounded raw
* attention cache row in one dispatch. */ * attention cache row in one dispatch. */
@@ -1593,7 +2047,9 @@ int ds4_gpu_compressor_update_tensor(
float beta_fast, float beta_fast,
float beta_slow, float beta_slow,
float rms_eps, float rms_eps,
bool state_already_stored); bool state_already_stored,
bool decode_one_token,
bool defer_finalize);
int ds4_gpu_compressor_store_batch_tensor( int ds4_gpu_compressor_store_batch_tensor(
const ds4_gpu_tensor *kv, const ds4_gpu_tensor *kv,
@@ -2134,6 +2590,7 @@ int ds4_gpu_glm_routed_moe_one_tensor(
const ds4_gpu_tensor *weights, const ds4_gpu_tensor *weights,
uint32_t n_total_expert, uint32_t n_total_expert,
uint32_t n_expert, uint32_t n_expert,
float swiglu_clamp,
uint32_t layer_index, uint32_t layer_index,
const ds4_gpu_tensor *x, const ds4_gpu_tensor *x,
bool force_resident); bool force_resident);
@@ -2162,6 +2619,7 @@ int ds4_gpu_glm_routed_moe_batch_tensor(
const ds4_gpu_tensor *weights, const ds4_gpu_tensor *weights,
uint32_t n_total_expert, uint32_t n_total_expert,
uint32_t n_expert, uint32_t n_expert,
float swiglu_clamp,
uint32_t layer_index, uint32_t layer_index,
const ds4_gpu_tensor *x, const ds4_gpu_tensor *x,
uint32_t n_tokens, uint32_t n_tokens,
@@ -2192,6 +2650,7 @@ int ds4_gpu_glm_routed_moe_batch_direct_scalar_q4_tensor(
const ds4_gpu_tensor *weights, const ds4_gpu_tensor *weights,
uint32_t n_total_expert, uint32_t n_total_expert,
uint32_t n_expert, uint32_t n_expert,
float swiglu_clamp,
uint32_t layer_index, uint32_t layer_index,
const ds4_gpu_tensor *x, const ds4_gpu_tensor *x,
uint32_t n_tokens, uint32_t n_tokens,
@@ -2199,6 +2658,10 @@ int ds4_gpu_glm_routed_moe_batch_direct_scalar_q4_tensor(
int ds4_gpu_routed_moe_set_selected_override(const int32_t *selected, uint32_t n_selected); int ds4_gpu_routed_moe_set_selected_override(const int32_t *selected, uint32_t n_selected);
void ds4_gpu_set_glm_mtp_verify_mode(bool enabled); void ds4_gpu_set_glm_mtp_verify_mode(bool enabled);
#ifdef DS4_ROCM_BUILD
int ds4_gpu_dspark_gfx1151_fast_path(void);
void ds4_gpu_set_dspark_verify_mode(bool enabled);
#endif
int ds4_gpu_matmul_q8_0_kslice_hc_expand_add_tensor( int ds4_gpu_matmul_q8_0_kslice_hc_expand_add_tensor(
ds4_gpu_tensor *out_hc, ds4_gpu_tensor *out_hc,
@@ -2393,18 +2856,6 @@ int ds4_gpu_hc_weighted_sum_tensor(
uint32_t n_embd, uint32_t n_embd,
uint32_t n_hc); uint32_t n_hc);
int ds4_gpu_hc_weighted_sum_norm_tensor(
ds4_gpu_tensor *out,
ds4_gpu_tensor *norm_out,
const ds4_gpu_tensor *residual_hc,
const ds4_gpu_tensor *weights,
const void *model_map,
uint64_t model_size,
uint64_t norm_weight_offset,
uint32_t n_embd,
uint32_t n_hc,
float norm_eps);
int ds4_gpu_hc_weighted_sum_split_tensor( int ds4_gpu_hc_weighted_sum_split_tensor(
ds4_gpu_tensor *out, ds4_gpu_tensor *out,
const ds4_gpu_tensor *residual_hc, const ds4_gpu_tensor *residual_hc,
@@ -2445,6 +2896,17 @@ int ds4_gpu_hc_split_weighted_sum_norm_tensor(
float eps, float eps,
float norm_eps); float norm_eps);
int ds4_gpu_hc_rms_norm_mix_f16_available(void);
int ds4_gpu_hc_rms_norm_mix_f16_tensor(
ds4_gpu_tensor *out,
const ds4_gpu_tensor *x,
const void *model_map,
uint64_t model_size,
uint64_t weight_offset,
uint32_t n,
uint32_t out_dim,
float eps);
/* Batched HC RMSNorm followed by its narrow F16 mixer projection. On the /* Batched HC RMSNorm followed by its narrow F16 mixer projection. On the
* tuned Metal path, scale_scratch stores one float per row instead of the * tuned Metal path, scale_scratch stores one float per row instead of the
* full normalized HC tensor; other shapes retain the established fallback. */ * full normalized HC tensor; other shapes retain the established fallback. */
@@ -2460,6 +2922,29 @@ int ds4_gpu_hc_rms_scale_project_f16_tensor(
uint32_t n_rows, uint32_t n_rows,
float eps); float eps);
#ifdef __APPLE__
int ds4_gpu_hc_rms_norm_mix_split_norm_f16_tensor(
ds4_gpu_tensor *mix,
ds4_gpu_tensor *out,
ds4_gpu_tensor *norm_out,
ds4_gpu_tensor *split,
const ds4_gpu_tensor *residual_hc,
const void *model_map,
uint64_t model_size,
uint64_t mix_weight_offset,
uint64_t scale_offset,
uint64_t base_offset,
uint64_t norm_weight_offset,
uint32_t n,
uint32_t mix_dim,
uint32_t n_embd,
uint32_t n_hc,
uint32_t sinkhorn_iters,
float eps,
float hc_eps,
float norm_eps);
#endif
int ds4_gpu_output_hc_weights_tensor( int ds4_gpu_output_hc_weights_tensor(
ds4_gpu_tensor *out, ds4_gpu_tensor *out,
const ds4_gpu_tensor *pre, const ds4_gpu_tensor *pre,
@@ -2596,6 +3081,174 @@ int ds4_gpu_matmul_q8_0_hc_expand_tensor(
uint32_t n_embd, uint32_t n_embd,
uint32_t n_hc); uint32_t n_hc);
int ds4_gpu_glm53_embedding_bf16(
ds4_gpu_tensor *out,
const void *model_map,
uint64_t model_size,
uint64_t weight_offset,
const ds4_gpu_tensor *token_ids,
uint32_t n_tokens,
uint32_t n_embd,
uint32_t n_vocab);
int ds4_gpu_glm53_matmul_bf16(
ds4_gpu_tensor *out,
const void *model_map,
uint64_t model_size,
uint64_t weight_offset,
uint32_t in_dim,
uint32_t out_dim,
const ds4_gpu_tensor *x,
uint32_t n_rows);
int ds4_gpu_glm53_matmul_bf16_qkv(
ds4_gpu_tensor *out_q,
ds4_gpu_tensor *out_k,
ds4_gpu_tensor *out_v,
const void *model_map,
uint64_t model_size,
uint64_t weight_q_offset,
uint64_t weight_k_offset,
uint64_t weight_v_offset,
uint32_t in_dim,
uint32_t out_dim,
const ds4_gpu_tensor *x);
#ifndef DS4_GLM53_VISION_TYPES_DEFINED
#define DS4_GLM53_VISION_TYPES_DEFINED
#define DS4_GLM53_VISION_LAYERS 24u
typedef struct {
uint64_t norm1;
uint64_t qkv_weight;
uint64_t qkv_bias;
uint64_t q_norm;
uint64_t k_norm;
uint64_t attn_proj_weight;
uint64_t attn_proj_bias;
uint64_t norm2;
uint64_t gate_weight;
uint64_t gate_bias;
uint64_t up_weight;
uint64_t up_bias;
uint64_t down_weight;
uint64_t down_bias;
} ds4_glm53_vision_layer_weights;
typedef struct {
uint64_t patch_weight;
uint64_t patch_bias;
uint64_t post_norm;
uint64_t downsample_weight;
uint64_t downsample_bias;
uint64_t merger_proj;
uint64_t merger_norm;
uint64_t merger_norm_bias;
uint64_t merger_gate;
uint64_t merger_up;
uint64_t merger_down;
ds4_glm53_vision_layer_weights layer[DS4_GLM53_VISION_LAYERS];
} ds4_glm53_vision_weights;
#endif
/* Encode normalized, block-major image patches into 4096-wide language-model
* embeddings. GPU implementations keep every intermediate on device. */
int ds4_gpu_glm53_vision_encode(
float *out,
const float *patches,
uint32_t grid_h,
uint32_t grid_w,
const void *model_map,
uint64_t model_size,
const ds4_glm53_vision_weights *weights);
/* Replace token rows with projected image embeddings and repeat each row into
* every GLM hyperconnection stream. Must be called in an active command batch. */
int ds4_gpu_glm53_scatter_image_hc(
ds4_gpu_tensor *hc,
const ds4_gpu_tensor *image,
uint32_t dst_row,
uint32_t image_row,
uint32_t rows,
uint32_t total_rows,
uint32_t n_embd,
uint32_t n_hc);
/* GLM-5.3 Kimi Delta Attention. Recurrent and convolution state stay FP32. */
int ds4_gpu_glm53_kda_decode(
ds4_gpu_tensor *out,
ds4_gpu_tensor *conv_state,
ds4_gpu_tensor *recurrent_state,
const ds4_gpu_tensor *q,
const ds4_gpu_tensor *k,
const ds4_gpu_tensor *v,
const ds4_gpu_tensor *raw_gate,
const ds4_gpu_tensor *raw_beta,
const ds4_gpu_tensor *output_gate,
const void *model_map,
uint64_t model_size,
uint64_t q_conv_offset,
uint64_t k_conv_offset,
uint64_t v_conv_offset,
uint64_t a_log_offset,
uint64_t dt_bias_offset,
uint64_t output_norm_offset,
uint32_t n_heads,
uint32_t n_rows,
float gate_lower_bound,
float norm_eps);
int ds4_gpu_glm53_kda_prefill(
ds4_gpu_tensor *out,
ds4_gpu_tensor *conv_state,
ds4_gpu_tensor *recurrent_state,
ds4_gpu_tensor *q,
ds4_gpu_tensor *k,
ds4_gpu_tensor *v,
ds4_gpu_tensor *raw_gate,
const ds4_gpu_tensor *raw_beta,
const ds4_gpu_tensor *output_gate,
const void *model_map,
uint64_t model_size,
uint64_t q_conv_offset,
uint64_t k_conv_offset,
uint64_t v_conv_offset,
uint64_t a_log_offset,
uint64_t dt_bias_offset,
uint64_t output_norm_offset,
uint32_t n_heads,
uint32_t n_tokens,
float gate_lower_bound,
float norm_eps);
/* Decode-island CUDA graph capture (CUDA backend; Metal/ROCm/CPU stub it
* out and stay eager). Design ported from the Entrpi/ds4 batched-serving
* fork's per-layer decode graph capture. The key identifies a captured
* island: layer, island index, and the activation buffers whose addresses
* the captured kernels bake in. ds4_cuda.cu mirrors this struct
* byte-for-byte (it does not include this header); keep both in sync. */
typedef struct ds4_decode_graph_key {
uint32_t il;
uint32_t island; /* 0: layer top to pre-rope; 1: attn-out to layer end */
uint32_t variant;
uint32_t _pad;
void *cur_hc;
void *after_attn_hc;
void *after_ffn_hc;
void *attn_norm;
} ds4_decode_graph_key;
int ds4_gpu_decode_graphs_supported(void);
/* 1: replayed (island already executed; skip encoding it)
* 0: capturing (encode the island, then call _end)
* -1: run eagerly */
int ds4_gpu_decode_graph_begin(const ds4_decode_graph_key *key);
/* 0: capture committed and launched; -1: capture failed (entry retired;
* the caller must re-encode the island eagerly -- no work was executed). */
int ds4_gpu_decode_graph_end(const ds4_decode_graph_key *key);
void ds4_gpu_decode_graph_abort(const ds4_decode_graph_key *key);
void ds4_gpu_decode_graphs_invalidate(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+7095 -899
View File
File diff suppressed because it is too large Load Diff
-1385
View File
File diff suppressed because it is too large Load Diff
-33
View File
@@ -1,33 +0,0 @@
#ifndef DS4_WEB_H
#define DS4_WEB_H
#include <stddef.h>
#include <stdbool.h>
typedef int (*ds4_web_confirm_fn)(void *privdata, const char *message,
char *err, size_t err_len);
typedef void (*ds4_web_log_fn)(void *privdata, const char *message);
typedef bool (*ds4_web_cancel_fn)(void *privdata);
typedef struct {
const char *home_dir;
int port;
ds4_web_confirm_fn confirm;
void *confirm_privdata;
ds4_web_log_fn log;
void *log_privdata;
ds4_web_cancel_fn cancel;
void *cancel_privdata;
} ds4_web_config;
typedef struct ds4_web ds4_web;
ds4_web *ds4_web_create(const ds4_web_config *cfg);
void ds4_web_free(ds4_web *web);
char *ds4_web_google_search(ds4_web *web, const char *query,
char *err, size_t err_len);
char *ds4_web_visit_page(ds4_web *web, const char *url,
char *err, size_t err_len);
#endif
+27
View File
@@ -0,0 +1,27 @@
[[IgnoredVulns]]
id = "RUSTSEC-2024-0436"
reason = "INFO Unmaintained with no patched version; transitive through Iced 0.14's wgpu Metal backend."
[[IgnoredVulns]]
id = "RUSTSEC-2026-0206"
reason = "INFO Unmaintained with no patched version; transitive through Iced 0.14's SVG renderer."
[[IgnoredVulns]]
id = "RUSTSEC-2026-0192"
reason = "INFO Unmaintained with no patched version; transitive through Iced 0.14's text and SVG renderers."
[[IgnoredVulns]]
id = "RUSTSEC-2026-0253"
reason = "No compatible Iced 0.14 upgrade; Cryoglyph does not call the affected LruCache::pop API."
[[IgnoredVulns]]
id = "RUSTSEC-2023-0071"
reason = "No patched rsa release; the release packager only creates ad-hoc signatures without RSA keys."
[[IgnoredVulns]]
id = "RUSTSEC-2020-0095"
reason = "INFO Unmaintained with no patched version; release-only transitive dependency through apple-codesign."
[[IgnoredVulns]]
id = "RUSTSEC-2025-0134"
reason = "INFO Unmaintained with no patched version; transitive through apple-codesign's unused remote-signing stack."
+6 -6
View File
@@ -45,7 +45,7 @@ def post(path, payload):
first = post( first = post(
"/v1/chat/completions", "/v1/chat/completions",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"tools": [chat_tool], "tools": [chat_tool],
"reasoning_effort": "none", "reasoning_effort": "none",
@@ -58,7 +58,7 @@ print("chat-first", json.dumps(first["usage"], separators=(",", ":")))
second = post( second = post(
"/v1/chat/completions", "/v1/chat/completions",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [ "messages": [
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
{"role": "assistant", "content": "", "tool_calls": [call]}, {"role": "assistant", "content": "", "tool_calls": [call]},
@@ -79,7 +79,7 @@ print("chat", json.dumps(second, separators=(",", ":")))
first = post( first = post(
"/v1/messages", "/v1/messages",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"tools": [anthropic_tool], "tools": [anthropic_tool],
"thinking": {"type": "disabled"}, "thinking": {"type": "disabled"},
@@ -92,7 +92,7 @@ print("anthropic-first", json.dumps(first["usage"], separators=(",", ":")))
second = post( second = post(
"/v1/messages", "/v1/messages",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [ "messages": [
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
{"role": "assistant", "content": [call]}, {"role": "assistant", "content": [call]},
@@ -118,7 +118,7 @@ print("anthropic", json.dumps(second, separators=(",", ":")))
first = post( first = post(
"/v1/responses", "/v1/responses",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"input": prompt, "input": prompt,
"tools": [responses_tool], "tools": [responses_tool],
"reasoning": {"effort": "none"}, "reasoning": {"effort": "none"},
@@ -131,7 +131,7 @@ print("responses-first", json.dumps(first["usage"], separators=(",", ":")))
second = post( second = post(
"/v1/responses", "/v1/responses",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"input": [ "input": [
{ {
"type": "message", "type": "message",
+3 -3
View File
@@ -16,7 +16,7 @@ cases = [
"chat", "chat",
"/v1/chat/completions", "/v1/chat/completions",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [ "messages": [
{ {
"role": "user", "role": "user",
@@ -42,7 +42,7 @@ cases = [
"anthropic", "anthropic",
"/v1/messages", "/v1/messages",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [ "messages": [
{ {
"role": "user", "role": "user",
@@ -65,7 +65,7 @@ cases = [
"responses", "responses",
"/v1/responses", "/v1/responses",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"input": "Call echo with text hi. Do not answer normally.", "input": "Call echo with text hi. Do not answer normally.",
"tools": [ "tools": [
{ {
+3 -3
View File
@@ -11,7 +11,7 @@ cases = [
"chat", "chat",
"/v1/chat/completions", "/v1/chat/completions",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"reasoning_effort": "low", "reasoning_effort": "low",
"temperature": 0, "temperature": 0,
@@ -22,7 +22,7 @@ cases = [
"anthropic", "anthropic",
"/v1/messages", "/v1/messages",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"thinking": {"type": "enabled", "budget_tokens": 64}, "thinking": {"type": "enabled", "budget_tokens": 64},
"output_config": {"effort": "low"}, "output_config": {"effort": "low"},
@@ -34,7 +34,7 @@ cases = [
"responses", "responses",
"/v1/responses", "/v1/responses",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"input": prompt, "input": prompt,
"reasoning": {"effort": "low", "summary": "auto"}, "reasoning": {"effort": "low", "summary": "auto"},
"temperature": 0, "temperature": 0,
+7 -2
View File
@@ -10,10 +10,15 @@ import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent ROOT = pathlib.Path(__file__).resolve().parent.parent
HARDWARE_TESTS = ( HARDWARE_TESTS = (
"flash_resident_and_ssd_streaming_choose_the_same_tokens", "flash_resident_and_ssd_streaming_choose_the_same_tokens",
"legacy_mtp_runs_a_target_owned_greedy_cycle", "flash_ssd_streaming_maps_batched_prefill_layers",
"resident_multi_session_switching_preserves_each_kv_frontier",
"dspark_runs_a_target_owned_greedy_cycle", "dspark_runs_a_target_owned_greedy_cycle",
"ssd_streaming_supports_legacy_mtp_and_dspark", "ssd_streaming_supports_dspark",
"directional_steering_matches_the_ds4_token_oracle", "directional_steering_matches_the_ds4_token_oracle",
"resident_and_streamed_glm_match_ds4_decode_oracles",
"streamed_glm_uses_ds4_indexed_prefill_for_long_prompts",
"glm_mtp_preserves_target_tokens_and_drafts",
"pro_ssd_streaming_full_layer_and_selected_batch_agree",
) )
ENDPOINT_SCRIPTS = ( ENDPOINT_SCRIPTS = (
"endpoint_parity.py", "endpoint_parity.py",
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Regenerate the Rust expert hotlists from a DS4 source checkout."""
import argparse
import pathlib
import re
ARRAY = re.compile(
r"static const uint16_t ds4_default_streaming_hotlist_(\w+)\[\]\[2\] = \{(.*?)\n\};",
re.DOTALL,
)
PAIR = re.compile(r"\{(\d+),\s*(\d+)\}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=pathlib.Path)
parser.add_argument("output", type=pathlib.Path)
args = parser.parse_args()
texts = [
(args.source / "ds4_streaming_hotlist.inc").read_text(),
(args.source / "ds4_streaming_hotlist_glm52.inc").read_text(),
]
arrays = {
name: PAIR.findall(body)
for text in texts
for name, body in ARRAY.findall(text)
}
names = (("PRO", "pro"), ("FLASH", "flash"), ("GLM52", "glm52"))
lines = ["// Generated mechanically by scripts/import_hotlists.py.\n"]
for constant, source_name in names:
lines.append(f"pub(super) const {constant}: &[(u16, u16)] = &[\n")
lines.extend(f" ({layer}, {expert}),\n" for layer, expert in arrays[source_name])
lines.append("];\n")
args.output.write_text("".join(lines))
if __name__ == "__main__":
main()
+3 -3
View File
@@ -28,7 +28,7 @@ def chat(base_url, prompt):
base_url, base_url,
"/v1/chat/completions", "/v1/chat/completions",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"reasoning_effort": "none", "reasoning_effort": "none",
"temperature": 0, "temperature": 0,
@@ -50,7 +50,7 @@ def anthropic(base_url, prompt):
base_url, base_url,
"/v1/messages", "/v1/messages",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"messages": [{"role": "user", "content": prompt}], "messages": [{"role": "user", "content": prompt}],
"thinking": {"type": "disabled"}, "thinking": {"type": "disabled"},
"temperature": 0, "temperature": 0,
@@ -69,7 +69,7 @@ def responses(base_url, prompt):
base_url, base_url,
"/v1/responses", "/v1/responses",
{ {
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash-0731",
"input": prompt, "input": prompt,
"reasoning": {"effort": "none"}, "reasoning": {"effort": "none"},
"temperature": 0, "temperature": 0,
+1217
View File
File diff suppressed because it is too large Load Diff
+451
View File
@@ -0,0 +1,451 @@
use super::*;
pub(super) fn resolve(value: &Value, data: &Value) -> Result<Value, String> {
resolve_at(value, data, data)
}
pub(super) fn resolve_at(value: &Value, data: &Value, context: &Value) -> Result<Value, String> {
if let Some(path) = value
.as_object()
.and_then(|value| value.get("path"))
.and_then(Value::as_str)
{
let source = if path.starts_with('/') { data } else { context };
let pointer = if path.starts_with('/') {
normalize_pointer(path).to_owned()
} else {
format!("/{path}")
};
return Ok(source.pointer(&pointer).cloned().unwrap_or(Value::Null));
}
if let Some(call) = value
.as_object()
.and_then(|value| value.get("call"))
.and_then(Value::as_str)
{
return evaluate(
call,
value.get("args").unwrap_or(&Value::Null),
data,
context,
);
}
match value {
Value::Array(values) => values
.iter()
.map(|value| resolve_at(value, data, context))
.collect::<Result<Vec<_>, _>>()
.map(Value::Array),
Value::Object(values) => values
.iter()
.map(|(key, value)| Ok((key.clone(), resolve_at(value, data, context)?)))
.collect::<Result<Map<_, _>, String>>()
.map(Value::Object),
_ => Ok(value.clone()),
}
}
pub(super) fn evaluate(
call: &str,
args: &Value,
data: &Value,
context: &Value,
) -> Result<Value, String> {
if !FUNCTIONS.contains(&call) {
return Err(format!("function `{call}` is not declared by the catalog"));
}
let args = args
.as_object()
.ok_or_else(|| format!("function `{call}` requires object args"))?;
let value = || resolve_at(args.get("value").unwrap_or(&Value::Null), data, context);
match call {
"required" => Ok(Value::Bool(match value()? {
Value::Null => false,
Value::String(value) => !value.is_empty(),
Value::Array(value) => !value.is_empty(),
Value::Object(value) => !value.is_empty(),
_ => true,
})),
"regex" => {
let pattern = args
.get("pattern")
.and_then(Value::as_str)
.ok_or_else(|| "regex requires a pattern".to_owned())?;
let regex = Regex::new(pattern).map_err(|error| format!("invalid regex: {error}"))?;
Ok(Value::Bool(regex.is_match(&display_value(&value()?))))
}
"length" => {
let length = display_value(&value()?).chars().count() as u64;
let min = args.get("min").and_then(Value::as_u64).unwrap_or(0);
let max = args.get("max").and_then(Value::as_u64).unwrap_or(u64::MAX);
Ok(Value::Bool((min..=max).contains(&length)))
}
"numeric" => {
let resolved = value()?;
let number = resolved
.as_f64()
.or_else(|| resolved.as_str().and_then(|value| value.parse().ok()));
let min = args
.get("min")
.and_then(Value::as_f64)
.unwrap_or(f64::NEG_INFINITY);
let max = args
.get("max")
.and_then(Value::as_f64)
.unwrap_or(f64::INFINITY);
Ok(Value::Bool(
number.is_some_and(|number| (min..=max).contains(&number)),
))
}
"email" => {
let email = display_value(&value()?);
let valid = Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
.unwrap()
.is_match(&email);
Ok(Value::Bool(valid))
}
"formatString" => Ok(Value::String(interpolate(
&display_value(&value()?),
data,
context,
)?)),
"formatNumber" => {
let number = value()?.as_f64().unwrap_or(0.0);
let digits = resolve_at(args.get("decimals").unwrap_or(&json!(2)), data, context)?
.as_f64()
.map(|value| value.max(0.0) as u64)
.unwrap_or(2)
.min(12) as usize;
let formatted = format!("{number:.digits$}");
Ok(Value::String(
if resolve_at(
args.get("grouping").unwrap_or(&Value::Bool(true)),
data,
context,
)?
.as_bool()
.unwrap_or(true)
{
group_number(&formatted)
} else {
formatted
},
))
}
"formatCurrency" => {
let number = value()?.as_f64().unwrap_or(0.0);
let currency = display_value(&resolve_at(
args.get("currency").unwrap_or(&Value::Null),
data,
context,
)?);
let digits = resolve_at(args.get("decimals").unwrap_or(&json!(2)), data, context)?
.as_f64()
.map(|value| value.max(0.0) as u64)
.unwrap_or(2)
.min(12) as usize;
let formatted = format!("{number:.digits$}");
let number = if resolve_at(
args.get("grouping").unwrap_or(&Value::Bool(true)),
data,
context,
)?
.as_bool()
.unwrap_or(true)
{
group_number(&formatted)
} else {
formatted
};
Ok(Value::String(format!("{currency} {number}")))
}
"formatDate" => {
let input = display_value(&value()?);
let date =
time::OffsetDateTime::parse(&input, &time::format_description::well_known::Rfc3339)
.map_err(|error| format!("formatDate requires an RFC 3339 value: {error}"))?;
let pattern = display_value(&resolve_at(
args.get("format").unwrap_or(&Value::Null),
data,
context,
)?);
Ok(Value::String(format_date(date, &pattern)))
}
"pluralize" => {
let number = value()?.as_f64().unwrap_or(0.0);
let key = if number == 0.0 && args.contains_key("zero") {
"zero"
} else if number == 1.0 && args.contains_key("one") {
"one"
} else if number == 2.0 && args.contains_key("two") {
"two"
} else {
"other"
};
resolve_at(args.get(key).unwrap_or(&Value::Null), data, context)
}
"and" | "or" => {
let values = args
.get("values")
.and_then(Value::as_array)
.ok_or_else(|| format!("{call} requires array `values`"))?;
let values = values
.iter()
.map(|value| {
resolve_at(value, data, context)?
.as_bool()
.ok_or_else(|| format!("{call} requires boolean values"))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Value::Bool(if call == "and" {
values.into_iter().all(|value| value)
} else {
values.into_iter().any(|value| value)
}))
}
"not" => {
Ok(Value::Bool(!value()?.as_bool().ok_or_else(|| {
"not requires a boolean value".to_owned()
})?))
}
"openUrl" => Ok(Value::Null),
"@index" => {
let index = TEMPLATE_INDEX
.with(Cell::get)
.ok_or_else(|| "@index is only available in a list template".to_owned())?;
let offset = args
.get("offset")
.map(|value| resolve_at(value, data, context))
.transpose()?
.and_then(|value| value.as_i64())
.unwrap_or(0);
Ok(json!(index as i64 + offset))
}
_ => unreachable!(),
}
}
fn interpolate(template: &str, data: &Value, context: &Value) -> Result<String, String> {
let mut output = String::new();
let mut offset = 0;
while let Some(relative) = template[offset..].find("${") {
let start = offset + relative;
if start > offset && template.as_bytes()[start - 1] == b'\\' {
output.push_str(&template[offset..start - 1]);
output.push_str("${");
offset = start + 2;
continue;
}
output.push_str(&template[offset..start]);
let end = expression_end(template, start + 2)
.ok_or_else(|| "formatString contains an unclosed expression".to_owned())?;
output.push_str(&display_value(&evaluate_expression(
&template[start + 2..end],
data,
context,
)?));
offset = end + 1;
}
output.push_str(&template[offset..]);
Ok(output)
}
fn expression_end(text: &str, mut offset: usize) -> Option<usize> {
let mut depth = 1;
let mut quote = None;
while offset < text.len() {
let character = text[offset..].chars().next()?;
if let Some(current) = quote {
if character == current && text.as_bytes().get(offset.wrapping_sub(1)) != Some(&b'\\') {
quote = None;
}
} else if matches!(character, '\'' | '"') {
quote = Some(character);
} else if text[offset..].starts_with("${") {
depth += 1;
offset += 2;
continue;
} else if character == '}' {
depth -= 1;
if depth == 0 {
return Some(offset);
}
}
offset += character.len_utf8();
}
None
}
fn evaluate_expression(expression: &str, data: &Value, context: &Value) -> Result<Value, String> {
let expression = expression.trim();
if let Some(open) = expression.find('(')
&& expression.ends_with(')')
{
let call = expression[..open].trim();
let mut args = Map::new();
for argument in split_expression_args(&expression[open + 1..expression.len() - 1]) {
let colon = top_level_separator(argument, ':')
.ok_or_else(|| format!("formatString argument `{argument}` must be named"))?;
let name = argument[..colon].trim();
if name.is_empty() {
return Err("formatString contains an empty argument name".into());
}
args.insert(
name.to_owned(),
expression_value(argument[colon + 1..].trim(), data, context)?,
);
}
return evaluate(call, &Value::Object(args), data, context);
}
let (source, pointer) = if expression.starts_with('/') {
(data, normalize_pointer(expression).to_owned())
} else {
(context, format!("/{expression}"))
};
Ok(source.pointer(&pointer).cloned().unwrap_or(Value::Null))
}
fn split_expression_args(input: &str) -> Vec<&str> {
let mut arguments = Vec::new();
let mut start = 0;
while let Some(relative) = top_level_separator(&input[start..], ',') {
arguments.push(input[start..start + relative].trim());
start += relative + 1;
}
if !input[start..].trim().is_empty() {
arguments.push(input[start..].trim());
}
arguments
}
fn top_level_separator(input: &str, separator: char) -> Option<usize> {
let mut round = 0;
let mut braces = 0;
let mut quote = None;
for (index, character) in input.char_indices() {
if let Some(current) = quote {
if character == current && input.as_bytes().get(index.wrapping_sub(1)) != Some(&b'\\') {
quote = None;
}
continue;
}
match character {
'\'' | '"' => quote = Some(character),
'(' => round += 1,
')' => round -= 1,
'{' => braces += 1,
'}' => braces -= 1,
_ if character == separator && round == 0 && braces == 0 => return Some(index),
_ => {}
}
}
None
}
fn expression_value(expression: &str, data: &Value, context: &Value) -> Result<Value, String> {
if expression.starts_with("${") && expression.ends_with('}') {
return evaluate_expression(&expression[2..expression.len() - 1], data, context);
}
if expression.starts_with('\'') && expression.ends_with('\'') && expression.len() >= 2 {
return Ok(Value::String(
expression[1..expression.len() - 1].to_owned(),
));
}
if let Ok(value) = serde_json::from_str(expression) {
return Ok(value);
}
if expression.starts_with('/') {
return Ok(data
.pointer(normalize_pointer(expression))
.cloned()
.unwrap_or(Value::Null));
}
Ok(Value::String(expression.to_owned()))
}
fn group_number(number: &str) -> String {
let (whole, fraction) = number.split_once('.').unwrap_or((number, ""));
let (sign, digits) = whole
.strip_prefix('-')
.map_or(("", whole), |digits| ("-", digits));
let mut grouped = String::with_capacity(number.len() + number.len() / 3);
grouped.push_str(sign);
for (index, digit) in digits.chars().enumerate() {
if index > 0 && (digits.len() - index).is_multiple_of(3) {
grouped.push(',');
}
grouped.push(digit);
}
if !fraction.is_empty() {
grouped.push('.');
grouped.push_str(fraction);
}
grouped
}
pub(super) fn format_date(date: time::OffsetDateTime, pattern: &str) -> String {
const MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const DAYS: [&str; 7] = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
let month = MONTHS[date.month() as usize - 1];
let day = DAYS[date.weekday().number_days_from_monday() as usize];
let hour_12 = match date.hour() % 12 {
0 => 12,
hour => hour,
};
let replacements = [
("EEEE", day.to_owned()),
("MMMM", month.to_owned()),
("yyyy", format!("{:04}", date.year())),
("MMM", month[..3].to_owned()),
("yy", format!("{:02}", date.year().rem_euclid(100))),
("MM", format!("{:02}", date.month() as u8)),
("dd", format!("{:02}", date.day())),
("HH", format!("{:02}", date.hour())),
("hh", format!("{hour_12:02}")),
("mm", format!("{:02}", date.minute())),
("ss", format!("{:02}", date.second())),
("E", day[..3].to_owned()),
("M", (date.month() as u8).to_string()),
("d", date.day().to_string()),
("H", date.hour().to_string()),
("h", hour_12.to_string()),
("a", if date.hour() < 12 { "AM" } else { "PM" }.to_owned()),
];
let mut output = String::new();
let mut remaining = pattern;
while !remaining.is_empty() {
if let Some((token, replacement)) = replacements
.iter()
.find(|(token, _)| remaining.starts_with(token))
{
output.push_str(replacement);
remaining = &remaining[token.len()..];
} else {
let character = remaining.chars().next().unwrap();
output.push(character);
remaining = &remaining[character.len_utf8()..];
}
}
output
}
+881
View File
@@ -0,0 +1,881 @@
use super::{Surface, bound_value_at, display_value, validate_surface_composition};
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, html};
use serde_json::{Map, Value};
use std::collections::{BTreeSet, HashMap};
use std::fmt::Write;
const WIDTH: usize = 900;
const HEIGHT: usize = 720;
const COLORS: [&str; 6] = [
"#75beff", "#89d185", "#d18616", "#f14c4c", "#b180d7", "#e2c440",
];
pub(crate) fn surface_svg(
surface: &Surface,
images: &HashMap<String, String>,
) -> Result<String, String> {
validate_surface_composition(surface)?;
let renderer = Renderer { surface, images };
let content = renderer.component("root", &surface.data, BTreeSet::new());
let agent = surface
.surface_properties
.get("agentDisplayName")
.and_then(Value::as_str)
.unwrap_or("A2UI");
let label = format!("{agent} surface {}", surface.id);
let mut svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" class="a2ui-snapshot" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" aria-label="{}"><foreignObject x="0" y="0" width="{WIDTH}" height="{HEIGHT}"><div xmlns="http://www.w3.org/1999/xhtml" class="a2ui-document"><style>{}</style><header><strong>{}</strong><span>{}</span></header><main>"#,
escaped(&label),
STYLE,
escaped(agent),
escaped(&surface.id),
);
svg.push_str(&content);
svg.push_str("</main></div></foreignObject></svg>");
Ok(svg)
}
const STYLE: &str = r#"
*{box-sizing:border-box} .a2ui-document{height:100%;overflow:auto;background:#17181b;color:#e8e8ea;font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;border:1px solid #34363d;border-radius:12px}.a2ui-document>header{position:sticky;top:0;z-index:2;display:flex;justify-content:space-between;padding:11px 14px;background:#202125;border-bottom:1px solid #34363d;color:#aeb0b6;font-size:12px}.a2ui-document>main{display:flex;flex-direction:column;gap:10px;padding:16px}.row,.column,.list{display:flex;gap:10px}.row,.list.horizontal{flex-direction:row;align-items:flex-start}.column,.list{flex-direction:column}.card,.map,.table{padding:13px;background:#202125;border:1px solid #34363d;border-radius:9px}.button,.chip{display:inline-block;padding:7px 11px;background:#30323a;border:1px solid #464951;border-radius:7px}.button.primary{background:#416a91}.button.borderless{background:transparent;border-color:transparent}.field{display:flex;flex-direction:column;gap:5px}.field-label,.muted,.chart-kind,.caption{color:#999ba3;font-size:12px}.input{min-height:34px;padding:7px 9px;background:#101114;border:1px solid #393b42;border-radius:6px;white-space:pre-wrap}.check{display:flex;gap:7px;align-items:center}.check-mark{width:18px;height:18px;text-align:center;border:1px solid #555862;border-radius:5px}.divider{height:1px;background:#3a3c43}.divider.vertical{width:1px;height:auto;min-height:32px}.metric-value{font-size:28px}.error{color:#ff8585}.bar{display:grid;grid-template-columns:minmax(80px,150px) 1fr auto;gap:8px;align-items:center}.bar-track{height:9px;overflow:hidden;background:#30323a;border-radius:5px}.bar-fill{height:100%;background:#75beff}.pie{width:150px;height:150px;border-radius:50%}.donut{position:relative}.donut:after{content:"";position:absolute;inset:34px;background:#17181b;border-radius:50%}.legend{display:flex;flex-wrap:wrap;gap:8px 14px}.swatch{display:inline-block;width:10px;height:10px;margin-right:5px;border-radius:2px}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:7px 9px;border:1px solid #3a3c43;text-align:left}.timeline-item,.location,.mind-node{display:flex;gap:8px}.tabs>details{margin-top:7px;padding:7px 9px;border:1px solid #34363d;border-radius:7px}.tabs summary{cursor:pointer}.media{padding:10px;border:1px solid #34363d;border-radius:7px}.surface-image{display:block;max-width:100%;max-height:260px;margin:auto;border-radius:7px}.chart,.timeline,.mindmap{display:flex;flex-direction:column;gap:8px}h1,h2,h3,p{margin-top:0}pre{overflow:auto}a{color:#8bbceb}
"#;
struct Renderer<'a> {
surface: &'a Surface,
images: &'a HashMap<String, String>,
}
impl Renderer<'_> {
fn component(&self, id: &str, context: &Value, mut ancestors: BTreeSet<String>) -> String {
if !ancestors.insert(id.to_owned()) {
return format!(
"<div class=\"error\">Cyclic component {}</div>",
escaped(id)
);
}
let Some(component) = self.surface.components.get(id).and_then(Value::as_object) else {
return format!(
"<div class=\"error\">Missing component {}</div>",
escaped(id)
);
};
let kind = component
.get("component")
.and_then(Value::as_str)
.unwrap_or("Unknown");
match kind {
"Text" => {
let content = safe_markdown(&self.value(component, "text", context));
if component.get("variant").and_then(Value::as_str) == Some("caption") {
format!("<div class=\"caption\">{content}</div>")
} else {
content
}
}
"Image" => self.image(component, context),
"Video" | "AudioPlayer" => self.media(component, context, kind),
"Icon" => format!(
"<span aria-label=\"{}\">{}</span>",
escaped(&self.value(component, "name", context)),
icon(&self.value(component, "name", context))
),
"Divider" => format!(
"<div class=\"divider{}\"></div>",
if component.get("axis").and_then(Value::as_str) == Some("vertical") {
" vertical"
} else {
""
}
),
"Row" | "Column" | "List" => {
let horizontal = kind == "Row"
|| (kind == "List"
&& component.get("direction").and_then(Value::as_str)
== Some("horizontal"));
let class = if kind == "List" {
if horizontal {
"list horizontal"
} else {
"list"
}
} else if kind == "Row" {
"row"
} else {
"column"
};
format!(
"<div class=\"{class}\"{}>{}</div>",
layout_style(component),
self.children(component.get("children"), context, &ancestors)
)
}
"Card" => format!(
"<div class=\"card\">{}</div>",
self.child(component, "child", context, &ancestors)
),
"Tabs" => self.tabs(component, context, &ancestors),
"Modal" => format!(
"<details><summary>{}</summary><div class=\"card\">{}</div></details>",
self.child(component, "trigger", context, &ancestors),
self.child(component, "content", context, &ancestors)
),
"Button" => self.with_error(
component,
format!(
"<span class=\"button {}\">{}</span>",
component
.get("variant")
.and_then(Value::as_str)
.unwrap_or("default"),
self.child(component, "child", context, &ancestors)
),
),
"TextField" => self.text_field(component, context),
"CheckBox" => self.checkbox(component, context),
"Slider" => self.slider(component, context),
"DateTimeInput" => self.with_error(
component,
self.field(
&self.value(component, "label", context),
&self.value(component, "value", context),
),
),
"ChoicePicker" => self.choices(component, context),
"Chart" => self.chart(component, context),
"Table" => self.table(component, context),
"Metric" => self.metric(component, context),
"Timeline" => self.timeline(component, context),
"Map" => self.map(component, context),
"MindMap" => self.mind_map(component, context),
"Form" => format!(
"<section class=\"card column\"><strong>{}</strong>{}<span class=\"button\">{}</span></section>",
escaped(&self.value(component, "title", context)),
self.children(component.get("children"), context, &ancestors),
escaped(
component
.get("submitLabel")
.and_then(Value::as_str)
.unwrap_or("Submit")
)
),
_ => format!(
"<div class=\"error\">Unsupported component {}</div>",
escaped(kind)
),
}
}
fn value(&self, component: &Map<String, Value>, key: &str, context: &Value) -> String {
display_value(&bound_value_at(
component.get(key),
&self.surface.data,
context,
))
}
fn with_error(&self, component: &Map<String, Value>, mut content: String) -> String {
if let Some(error) = super::first_failed_check(component, &self.surface.data) {
let _ = write!(content, "<div class=\"error\">{}</div>", escaped(&error));
}
content
}
fn child(
&self,
component: &Map<String, Value>,
key: &str,
context: &Value,
ancestors: &BTreeSet<String>,
) -> String {
component
.get(key)
.and_then(Value::as_str)
.map(|id| self.component(id, context, ancestors.clone()))
.unwrap_or_default()
}
fn children(
&self,
children: Option<&Value>,
context: &Value,
ancestors: &BTreeSet<String>,
) -> String {
if let Some(children) = children.and_then(Value::as_array) {
return children
.iter()
.filter_map(Value::as_str)
.map(|id| self.weighted_component(id, context, ancestors))
.collect();
}
let Some(template) = children.and_then(Value::as_object) else {
return String::new();
};
let id = template
.get("componentId")
.and_then(Value::as_str)
.unwrap_or("");
let path = template.get("path").and_then(Value::as_str).unwrap_or("/");
self.surface
.data
.pointer(path)
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
.map(|(index, item)| {
super::with_template_index(index, || self.weighted_component(id, item, ancestors))
})
.collect()
}
fn weighted_component(
&self,
id: &str,
context: &Value,
ancestors: &BTreeSet<String>,
) -> String {
let content = self.component(id, context, ancestors.clone());
self.surface
.components
.get(id)
.and_then(|component| component.get("weight"))
.and_then(Value::as_f64)
.filter(|weight| weight.is_finite() && *weight > 0.0)
.map_or(content.clone(), |weight| {
format!("<div style=\"flex:{weight} 1 0\">{content}</div>")
})
}
fn tabs(
&self,
component: &Map<String, Value>,
context: &Value,
ancestors: &BTreeSet<String>,
) -> String {
let mut output = String::from("<div class=\"tabs\">");
for (index, tab) in component
.get("tabs")
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
{
let title = display_value(&bound_value_at(
tab.get("title"),
&self.surface.data,
context,
));
let child = tab.get("child").and_then(Value::as_str).unwrap_or("");
let _ = write!(
output,
"<details{}><summary>{}</summary>{}</details>",
if index == 0 { " open" } else { "" },
escaped(&title),
self.component(child, context, ancestors.clone())
);
}
output.push_str("</div>");
output
}
fn image(&self, component: &Map<String, Value>, context: &Value) -> String {
let url = self.value(component, "url", context);
let description = self.value(component, "description", context);
let source = self
.images
.get(&url)
.map(String::as_str)
.or_else(|| url.starts_with("data:image/").then_some(url.as_str()));
source.map_or_else(
|| {
format!(
"<div class=\"media\">▧ {}<div class=\"muted\">{}</div></div>",
escaped(&description),
escaped(&url)
)
},
|source| {
format!(
"<img class=\"surface-image\" src=\"{}\" alt=\"{}\"/>",
escaped(source),
escaped(&description)
)
},
)
}
fn media(&self, component: &Map<String, Value>, context: &Value, kind: &str) -> String {
format!(
"<div class=\"media\">{} {}<div class=\"muted\">{}</div></div>",
if kind == "Video" { "" } else { "" },
escaped(&self.value(component, "description", context)),
escaped(&self.value(component, "url", context))
)
}
fn text_field(&self, component: &Map<String, Value>, context: &Value) -> String {
let mut value = self.value(component, "value", context);
if component.get("variant").and_then(Value::as_str) == Some("obscured") {
value = "••••••••".to_owned();
} else if value.is_empty() {
value = self.value(component, "placeholder", context);
}
self.with_error(
component,
self.field(&self.value(component, "label", context), &value),
)
}
fn field(&self, label: &str, value: &str) -> String {
format!(
"<div class=\"field\"><span class=\"field-label\">{}</span><div class=\"input\">{}</div></div>",
escaped(label),
escaped(value)
)
}
fn checkbox(&self, component: &Map<String, Value>, context: &Value) -> String {
let checked = bound_value_at(component.get("value"), &self.surface.data, context)
.as_bool()
.unwrap_or(false);
self.with_error(
component,
format!(
"<div class=\"check\"><span class=\"check-mark\">{}</span>{}</div>",
if checked { "" } else { "" },
escaped(&self.value(component, "label", context))
),
)
}
fn slider(&self, component: &Map<String, Value>, context: &Value) -> String {
let value = bound_value_at(component.get("value"), &self.surface.data, context)
.as_f64()
.unwrap_or(0.0);
let min = component.get("min").and_then(Value::as_f64).unwrap_or(0.0);
let max = component
.get("max")
.and_then(Value::as_f64)
.unwrap_or(100.0);
let percent = if max > min {
((value - min) / (max - min) * 100.0).clamp(0.0, 100.0)
} else {
0.0
};
self.with_error(component, format!(
"<div class=\"field\"><span class=\"field-label\">{}</span><div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:{percent:.2}%\"></div></div><span>{}</span></div>",
escaped(&self.value(component, "label", context)),
escaped(&display_value(&Value::from(value)))
))
}
fn choices(&self, component: &Map<String, Value>, context: &Value) -> String {
let selected = bound_value_at(component.get("value"), &self.surface.data, context);
let multiple =
component.get("variant").and_then(Value::as_str) == Some("multipleSelection");
let mut output = format!(
"<div class=\"field\"><span class=\"field-label\">{}</span><div class=\"row\">",
escaped(&self.value(component, "label", context))
);
for option in component
.get("options")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let value = option.get("value").unwrap_or(&Value::Null);
let active = selected
.as_array()
.is_some_and(|values| values.contains(value))
|| (!multiple && &selected == value);
let _ = write!(
output,
"<span class=\"chip\">{} {}</span>",
if active { "" } else { "" },
escaped(&option.get("label").map(display_value).unwrap_or_default())
);
}
output.push_str("</div></div>");
self.with_error(component, output)
}
fn chart(&self, component: &Map<String, Value>, context: &Value) -> String {
let series = bound_value_at(component.get("series"), &self.surface.data, context);
let series = series.as_array().map(Vec::as_slice).unwrap_or(&[]);
let chart_type = component
.get("chartType")
.and_then(Value::as_str)
.unwrap_or("bar");
let mut output = format!(
"<section class=\"chart\"><strong>{}</strong><span class=\"chart-kind\">{}</span>",
escaped(&self.value(component, "title", context)),
escaped(&chart_type.to_uppercase())
);
if matches!(chart_type, "pie" | "donut") {
let total = series
.iter()
.filter_map(|point| point.get("value")?.as_f64())
.filter(|value| *value > 0.0)
.sum::<f64>();
let mut position = 0.0;
let mut stops = Vec::new();
for (index, point) in series.iter().enumerate() {
let value = point
.get("value")
.and_then(Value::as_f64)
.unwrap_or(0.0)
.max(0.0);
let end = if total > 0.0 {
position + value / total * 100.0
} else {
position
};
stops.push(format!(
"{} {position:.3}% {end:.3}%",
COLORS[index % COLORS.len()]
));
position = end;
}
let _ = write!(
output,
"<div class=\"row\"><div class=\"pie {}\" style=\"background:conic-gradient({})\"></div><div class=\"legend\">",
if chart_type == "donut" { "donut" } else { "" },
stops.join(",")
);
for (index, point) in series.iter().enumerate() {
let _ = write!(
output,
"<span><i class=\"swatch\" style=\"background:{}\"></i>{} {}</span>",
COLORS[index % COLORS.len()],
escaped(&point.get("label").map(display_value).unwrap_or_default()),
escaped(&point.get("value").map(display_value).unwrap_or_default())
);
}
output.push_str("</div></div>");
} else if chart_type == "heatmap" {
let columns = series
.iter()
.flat_map(|row| {
row.get("segments")
.and_then(Value::as_array)
.into_iter()
.flatten()
})
.filter_map(|segment| segment.get("label").and_then(Value::as_str))
.fold(Vec::<String>::new(), |mut columns, label| {
if !columns.iter().any(|column| column == label) {
columns.push(label.to_owned());
}
columns
});
let max = series
.iter()
.flat_map(|row| {
row.get("segments")
.and_then(Value::as_array)
.into_iter()
.flatten()
})
.filter_map(|segment| segment.get("value").and_then(Value::as_f64))
.fold(0.0_f64, f64::max)
.max(1.0);
output.push_str("<table class=\"table\"><thead><tr><th></th>");
for column in &columns {
let _ = write!(output, "<th>{}</th>", escaped(column));
}
output.push_str("</tr></thead><tbody>");
for row in series {
let segments = row
.get("segments")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[]);
let _ = write!(
output,
"<tr><th>{}</th>",
escaped(&row.get("label").map(display_value).unwrap_or_default())
);
for column in &columns {
let value = segments
.iter()
.find(|segment| {
segment.get("label").and_then(Value::as_str) == Some(column)
})
.and_then(|segment| segment.get("value").and_then(Value::as_f64))
.unwrap_or(0.0);
let alpha = (value / max).clamp(0.0, 1.0);
let _ = write!(
output,
"<td style=\"background:rgba(183,72,72,{alpha:.3})\">{}</td>",
escaped(&display_value(&Value::from(value)))
);
}
output.push_str("</tr>");
}
output.push_str("</tbody></table>");
} else {
let max = series
.iter()
.filter_map(|point| point.get("value")?.as_f64())
.fold(0.0_f64, f64::max)
.max(1.0);
for point in series {
let value = point.get("value").and_then(Value::as_f64).unwrap_or(0.0);
let percent = (value / max * 100.0).clamp(0.0, 100.0);
let _ = write!(
output,
"<div class=\"bar\"><span>{}</span><div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:{percent:.2}%\"></div></div><span>{}</span></div>",
escaped(&point.get("label").map(display_value).unwrap_or_default()),
escaped(&point.get("value").map(display_value).unwrap_or_default())
);
if let Some(segments) = point.get("segments").and_then(Value::as_array) {
let details = segments
.iter()
.map(|segment| {
format!(
"{} {}",
segment.get("label").map(display_value).unwrap_or_default(),
segment.get("value").map(display_value).unwrap_or_default()
)
})
.collect::<Vec<_>>()
.join(" · ");
let _ = write!(output, "<div class=\"muted\">{}</div>", escaped(&details));
}
}
}
output.push_str("</section>");
output
}
fn table(&self, component: &Map<String, Value>, context: &Value) -> String {
let columns = bound_value_at(component.get("columns"), &self.surface.data, context);
let rows = bound_value_at(component.get("rows"), &self.surface.data, context);
let mut output = format!(
"<section><strong>{}</strong><table class=\"table\"><thead><tr>",
escaped(&self.value(component, "title", context))
);
for column in columns.as_array().into_iter().flatten() {
let _ = write!(output, "<th>{}</th>", escaped(&display_value(column)));
}
output.push_str("</tr></thead><tbody>");
for row in rows
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_array)
{
output.push_str("<tr>");
for cell in row {
let _ = write!(output, "<td>{}</td>", escaped(&display_value(cell)));
}
output.push_str("</tr>");
}
output.push_str("</tbody></table></section>");
output
}
fn metric(&self, component: &Map<String, Value>, context: &Value) -> String {
format!(
"<section><div class=\"muted\">{}</div><div class=\"metric-value\">{}</div><div class=\"muted\">{} {}</div></section>",
escaped(&self.value(component, "label", context)),
escaped(&self.value(component, "value", context)),
escaped(&self.value(component, "detail", context)),
escaped(&self.value(component, "trend", context))
)
}
fn timeline(&self, component: &Map<String, Value>, context: &Value) -> String {
let events = bound_value_at(component.get("events"), &self.surface.data, context);
let mut output = format!(
"<section class=\"timeline\"><strong>{}</strong>",
escaped(&self.value(component, "title", context))
);
for event in events.as_array().into_iter().flatten() {
let _ = write!(
output,
"<div class=\"timeline-item\"><span>●</span><div><strong>{}</strong><div class=\"muted\">{} · {} · {}</div></div></div>",
escaped(&event.get("title").map(display_value).unwrap_or_default()),
escaped(&event.get("time").map(display_value).unwrap_or_default()),
escaped(
&event
.get("description")
.map(display_value)
.unwrap_or_default()
),
escaped(&event.get("status").map(display_value).unwrap_or_default())
);
}
output.push_str("</section>");
output
}
fn map(&self, component: &Map<String, Value>, context: &Value) -> String {
let locations = bound_value_at(component.get("locations"), &self.surface.data, context);
let mut output = format!(
"<section class=\"map\"><strong>{}</strong>",
escaped(&self.value(component, "title", context))
);
for location in locations.as_array().into_iter().flatten() {
let _ = write!(
output,
"<div class=\"location\"><span>⌖</span><div><strong>{}</strong><div class=\"muted\">{}, {} · {}</div></div></div>",
escaped(&location.get("label").map(display_value).unwrap_or_default()),
escaped(
&location
.get("latitude")
.map(display_value)
.unwrap_or_default()
),
escaped(
&location
.get("longitude")
.map(display_value)
.unwrap_or_default()
),
escaped(
&location
.get("detail")
.map(display_value)
.unwrap_or_default()
)
);
}
output.push_str("</section>");
output
}
fn mind_map(&self, component: &Map<String, Value>, context: &Value) -> String {
let nodes = bound_value_at(component.get("nodes"), &self.surface.data, context);
let nodes = nodes.as_array().map(Vec::as_slice).unwrap_or(&[]);
let mut output = format!(
"<section class=\"mindmap\"><strong>{}</strong>",
escaped(&self.value(component, "title", context))
);
if let Some(root) = nodes.first() {
output.push_str(&mind_node(nodes, root, 0, BTreeSet::new()));
}
output.push_str("</section>");
output
}
}
fn mind_node(nodes: &[Value], node: &Value, depth: usize, mut seen: BTreeSet<String>) -> String {
let id = node.get("id").map(display_value).unwrap_or_default();
if !seen.insert(id) {
return "<div class=\"muted\">Cycle</div>".to_owned();
}
let mut output = format!(
"<div class=\"mind-node\" style=\"padding-left:{}px\"><span>{}</span><span>{}</span></div>",
depth * 18,
if depth == 0 { "" } else { "" },
escaped(&node.get("label").map(display_value).unwrap_or_default())
);
for child in node
.get("children")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
if let Some(child) = nodes
.iter()
.find(|candidate| candidate.get("id") == Some(child))
{
output.push_str(&mind_node(nodes, child, depth + 1, seen.clone()));
}
}
output
}
fn layout_style(component: &Map<String, Value>) -> String {
let justify = match component.get("justify").and_then(Value::as_str) {
Some("center") => "center",
Some("end") => "flex-end",
Some("spaceBetween") => "space-between",
Some("spaceAround") => "space-around",
Some("spaceEvenly") => "space-evenly",
_ => "flex-start",
};
let align = match component.get("align").and_then(Value::as_str) {
Some("center") => "center",
Some("end") => "flex-end",
Some("stretch") => "stretch",
_ => "flex-start",
};
format!(" style=\"justify-content:{justify};align-items:{align}\"")
}
fn safe_markdown(markdown: &str) -> String {
let options = Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS;
let parser = Parser::new_ext(markdown, options).scan(Vec::new(), |images, event| {
Some(match event {
Event::Html(html) | Event::InlineHtml(html) => Event::Text(html),
Event::Start(tag @ Tag::Image { .. }) => {
let embedded = matches!(
&tag,
Tag::Image { dest_url, .. } if dest_url.starts_with("data:image/")
);
images.push(embedded);
if embedded {
Event::Start(tag)
} else {
Event::Text("[Image: ".into())
}
}
Event::End(TagEnd::Image) => {
if images.pop().unwrap_or(false) {
Event::End(TagEnd::Image)
} else {
Event::Text("]".into())
}
}
event => event,
})
});
let mut output = String::new();
html::push_html(&mut output, parser);
output
}
fn escaped(text: &str) -> String {
let mut output = String::with_capacity(text.len());
for character in text.chars() {
match character {
'&' => output.push_str("&amp;"),
'<' => output.push_str("&lt;"),
'>' => output.push_str("&gt;"),
'"' => output.push_str("&quot;"),
'\'' => output.push_str("&#39;"),
_ => output.push(character),
}
}
output
}
fn icon(name: &str) -> &'static str {
match name {
"accountCircle" | "person" => "",
"add" => "+",
"arrowBack" => "",
"arrowForward" => "",
"attachFile" => "",
"calendarToday" | "event" => "",
"call" | "phone" => "",
"camera" => "",
"check" => "",
"close" => "×",
"delete" => "",
"download" => "",
"edit" => "",
"error" => "",
"fastForward" => "",
"favorite" => "",
"favoriteOff" => "",
"folder" => "",
"help" => "?",
"home" => "",
"info" => "",
"locationOn" => "",
"lock" => "",
"lockOpen" => "",
"mail" => "",
"menu" => "",
"moreVert" => "",
"moreHoriz" => "",
"notifications" => "",
"notificationsOff" => "",
"pause" => "",
"payment" => "¤",
"photo" => "",
"play" => "",
"print" => "",
"refresh" => "",
"rewind" => "",
"search" => "",
"send" => "",
"settings" => "",
"share" => "",
"shoppingCart" => "",
"skipNext" => "▸|",
"skipPrevious" => "|◂",
"star" => "",
"starHalf" | "starOff" => "",
"stop" => "",
"upload" => "",
"visibility" => "",
"visibilityOff" => "",
"volumeDown" => "",
"volumeMute" => "",
"volumeOff" => "×",
"volumeUp" => "",
"warning" => "",
_ => "",
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::BTreeMap;
#[test]
fn static_svg_renderer_covers_the_complete_catalog() {
let entries = [
json!({"id":"text","component":"Text","text":"Hello"}),
json!({"id":"image","component":"Image","url":"https://example.com/a.png","description":"Preview"}),
json!({"id":"icon","component":"Icon","name":"settings"}),
json!({"id":"video","component":"Video","url":"https://example.com/a.mp4"}),
json!({"id":"audio","component":"AudioPlayer","url":"https://example.com/a.mp3"}),
json!({"id":"divider","component":"Divider"}),
json!({"id":"row","component":"Row","children":[]}),
json!({"id":"list","component":"List","children":[]}),
json!({"id":"card","component":"Card","child":"text"}),
json!({"id":"modal","component":"Modal","trigger":"text","content":"text"}),
json!({"id":"tabs","component":"Tabs","tabs":[{"title":"One","child":"text"}]}),
json!({"id":"button","component":"Button","child":"text"}),
json!({"id":"field","component":"TextField","label":"Name","value":"Ada"}),
json!({"id":"check","component":"CheckBox","label":"Ready","value":true}),
json!({"id":"slider","component":"Slider","label":"Amount","value":4,"max":10}),
json!({"id":"date","component":"DateTimeInput","label":"When","value":"2026-08-31"}),
json!({"id":"choice","component":"ChoicePicker","label":"Pick","options":[{"label":"One","value":"one"}],"value":["one"]}),
json!({"id":"chart","component":"Chart","title":"Chart","chartType":"heatmap","series":[{"label":"A","segments":[{"label":"B","value":2}]}]}),
json!({"id":"table","component":"Table","title":"Table","columns":["A"],"rows":[["B"]]}),
json!({"id":"metric","component":"Metric","label":"Count","value":"2"}),
json!({"id":"timeline","component":"Timeline","title":"Timeline","events":[{"time":"Now","title":"Done"}]}),
json!({"id":"map","component":"Map","title":"Map","locations":[{"label":"Here","latitude":1,"longitude":2}]}),
json!({"id":"mind","component":"MindMap","title":"Mind","nodes":[{"id":"n","label":"Root","children":[]}]}),
json!({"id":"form","component":"Form","title":"Form","children":[]}),
];
let ids = entries
.iter()
.filter_map(|entry| entry.get("id").and_then(Value::as_str))
.map(str::to_owned)
.collect::<Vec<_>>();
let mut components = entries
.into_iter()
.map(|entry| (entry["id"].as_str().unwrap().to_owned(), entry))
.collect::<BTreeMap<_, _>>();
components.insert(
"root".into(),
json!({"id":"root","component":"Column","children":ids}),
);
let surface = Surface {
id: "catalog".into(),
catalog_id: super::super::CATALOG_ID.into(),
surface_properties: json!({}),
send_data_model: false,
components,
data: json!({}),
owner_message_id: 1,
};
let svg = surface_svg(&surface, &HashMap::new()).unwrap();
assert!(svg.starts_with("<svg"));
assert!(!svg.contains("Unsupported component"));
assert!(svg.contains("HEATMAP"));
assert!(svg.contains("Submit"));
}
}
+659
View File
@@ -0,0 +1,659 @@
use super::*;
pub(super) fn validate_component(
component: &Map<String, Value>,
catalog_id: &str,
) -> Result<(), String> {
let id = required_string(component, "id")?;
let kind = required_string(component, "component")?;
let allowed = if catalog_id == CATALOG_ID {
ds4_catalog()
.pointer("/components")
.and_then(Value::as_object)
.is_some_and(|components| components.contains_key(kind))
} else {
BASIC_COMPONENTS.contains(&kind)
};
if !allowed {
return Err(format!(
"component `{id}` uses `{kind}`, which is not in catalog `{catalog_id}`"
));
}
let required: &[&str] = match kind {
"Text" => &["text"],
"Image" => &["url"],
"Icon" => &["name"],
"Video" | "AudioPlayer" => &["url"],
"Row" | "Column" | "List" => &["children"],
"Card" => &["child"],
"Modal" => &["trigger", "content"],
"Tabs" => &["tabs"],
"Button" => &["child", "action"],
"TextField" => &["label"],
"CheckBox" => &["label", "value"],
"Slider" => &["value", "max"],
"DateTimeInput" => &["value"],
"ChoicePicker" => &["options", "value"],
"Chart" => &["chartType", "series"],
"Table" => &["columns", "rows"],
"Metric" => &["label", "value"],
"Timeline" => &["events"],
"Map" => &["locations"],
"MindMap" => &["nodes"],
"Form" => &["children"],
_ => &[],
};
if let Some(field) = required
.iter()
.find(|field| !component.contains_key(**field))
{
return Err(format!("component `{id}` ({kind}) requires `{field}`"));
}
let common = ["id", "component", "accessibility", "weight", "checks"];
let specific: &[&str] = match kind {
"Text" => &["text", "variant"],
"Image" => &["url", "description", "fit", "variant"],
"Icon" => &["name"],
"Video" => &["url", "posterUrl"],
"AudioPlayer" => &["url", "description"],
"Divider" => &["axis"],
"Row" | "Column" => &["children", "justify", "align"],
"List" => &["children", "direction", "align"],
"Card" => &["child"],
"Modal" => &["trigger", "content"],
"Tabs" => &["tabs"],
"Button" => &["child", "variant", "action"],
"TextField" => &["label", "value", "placeholder", "variant"],
"CheckBox" => &["label", "value"],
"Slider" => &["label", "min", "max", "value", "steps"],
"DateTimeInput" => &["label", "value", "enableDate", "enableTime", "min", "max"],
"ChoicePicker" => &[
"label",
"variant",
"options",
"value",
"displayStyle",
"filterable",
],
"Chart" => &["title", "chartType", "series"],
"Table" => &["title", "columns", "rows"],
"Metric" => &["label", "value", "detail", "trend"],
"Timeline" => &["title", "events"],
"Map" => &["title", "locations"],
"MindMap" => &["title", "nodes"],
"Form" => &["title", "children", "submitLabel", "action"],
_ => &[],
};
if let Some(field) = component
.keys()
.find(|field| !common.contains(&field.as_str()) && !specific.contains(&field.as_str()))
{
return Err(format!(
"component `{id}` ({kind}) contains unknown property `{field}`"
));
}
if let Some(accessibility) = component.get("accessibility") {
let accessibility = object(Some(accessibility), "accessibility")?;
for field in ["label", "description"] {
validate_optional_dynamic(accessibility, field, Value::is_string, id)?;
}
}
if component
.get("weight")
.is_some_and(|value| !value.is_number())
{
return Err(format!("component `{id}` weight must be a number"));
}
if component
.get("title")
.is_some_and(|value| !value.is_string())
{
return Err(format!("component `{id}` title must be a string"));
}
if catalog_id != CATALOG_ID
&& component.contains_key("checks")
&& !matches!(
kind,
"Button" | "TextField" | "CheckBox" | "ChoicePicker" | "Slider" | "DateTimeInput"
)
{
return Err(format!("component `{id}` ({kind}) does not support checks"));
}
match kind {
"Row" | "Column" | "List" | "Form" => {
validate_children(component.get("children").unwrap(), id)?
}
"Card" | "Button" => expect_string(component, "child", id)?,
"Modal" => {
expect_string(component, "trigger", id)?;
expect_string(component, "content", id)?;
}
"Tabs" => validate_tabs(component.get("tabs").unwrap(), id)?,
"ChoicePicker" => validate_options(component.get("options").unwrap(), id)?,
"Slider" => {
expect_number(component, "max", id)?;
if let Some(min) = component.get("min")
&& !min.is_number()
{
return Err(format!("component `{id}` min must be a number"));
}
if let Some(steps) = component.get("steps")
&& !steps.as_u64().is_some_and(|steps| steps > 0)
{
return Err(format!("component `{id}` steps must be a positive integer"));
}
}
_ => {}
}
match kind {
"Text" => validate_dynamic_field(component, "text", Value::is_string, id)?,
"Image" | "Video" | "AudioPlayer" => {
validate_dynamic_field(component, "url", Value::is_string, id)?;
validate_optional_dynamic(component, "description", Value::is_string, id)?;
validate_optional_dynamic(component, "posterUrl", Value::is_string, id)?;
}
"Icon" => validate_dynamic_field(component, "name", Value::is_string, id)?,
"TextField" | "DateTimeInput" => {
validate_optional_dynamic(component, "label", Value::is_string, id)?;
validate_optional_dynamic(component, "value", Value::is_string, id)?;
}
"CheckBox" => {
validate_dynamic_field(component, "label", Value::is_string, id)?;
validate_dynamic_field(component, "value", Value::is_boolean, id)?;
}
"Slider" => validate_dynamic_field(component, "value", Value::is_number, id)?,
"ChoicePicker" => {
validate_optional_dynamic(component, "label", Value::is_string, id)?;
validate_dynamic_field(
component,
"value",
|value| {
value
.as_array()
.is_some_and(|values| values.iter().all(Value::is_string))
},
id,
)?;
}
"Chart" | "Table" | "Timeline" | "Map" | "MindMap" => {
for field in required {
if *field != "chartType" {
validate_dynamic_field(component, field, Value::is_array, id)?;
}
}
}
_ => {}
}
match kind {
"Text" => validate_enum(component, "variant", &["caption", "body"], id)?,
"Image" => {
validate_enum(
component,
"fit",
&["contain", "cover", "fill", "none", "scaleDown"],
id,
)?;
validate_enum(
component,
"variant",
&[
"icon",
"avatar",
"smallFeature",
"mediumFeature",
"largeFeature",
"header",
],
id,
)?;
}
"Icon" if component.get("name").is_some_and(Value::is_string) => validate_enum(
component,
"name",
&[
"accountCircle",
"add",
"arrowBack",
"arrowForward",
"attachFile",
"calendarToday",
"call",
"camera",
"check",
"close",
"delete",
"download",
"edit",
"event",
"error",
"fastForward",
"favorite",
"favoriteOff",
"folder",
"help",
"home",
"info",
"locationOn",
"lock",
"lockOpen",
"mail",
"menu",
"moreVert",
"moreHoriz",
"notificationsOff",
"notifications",
"pause",
"payment",
"person",
"phone",
"photo",
"play",
"print",
"refresh",
"rewind",
"search",
"send",
"settings",
"share",
"shoppingCart",
"skipNext",
"skipPrevious",
"star",
"starHalf",
"starOff",
"stop",
"upload",
"visibility",
"visibilityOff",
"volumeDown",
"volumeMute",
"volumeOff",
"volumeUp",
"warning",
],
id,
)?,
"Divider" => validate_enum(component, "axis", &["horizontal", "vertical"], id)?,
"Row" | "Column" => {
validate_enum(
component,
"justify",
&[
"start",
"center",
"end",
"spaceBetween",
"spaceAround",
"spaceEvenly",
"stretch",
],
id,
)?;
validate_enum(
component,
"align",
&["start", "center", "end", "stretch"],
id,
)?;
}
"List" => {
validate_enum(component, "direction", &["vertical", "horizontal"], id)?;
validate_enum(
component,
"align",
&["start", "center", "end", "stretch"],
id,
)?;
}
"Button" => validate_enum(
component,
"variant",
&["default", "primary", "borderless"],
id,
)?,
"TextField" => validate_enum(
component,
"variant",
&["longText", "number", "shortText", "obscured"],
id,
)?,
"ChoicePicker" => {
validate_enum(
component,
"variant",
&["multipleSelection", "mutuallyExclusive"],
id,
)?;
validate_enum(component, "displayStyle", &["checkbox", "chips"], id)?;
optional_bool(component, "filterable")?;
}
"DateTimeInput" => {
optional_bool(component, "enableDate")?;
optional_bool(component, "enableTime")?;
validate_optional_dynamic(component, "min", Value::is_string, id)?;
validate_optional_dynamic(component, "max", Value::is_string, id)?;
}
"Chart" => validate_enum(
component,
"chartType",
&[
"bar",
"line",
"area",
"stackedBar",
"pie",
"donut",
"heatmap",
],
id,
)?,
_ => {}
}
if let Some(action) = component.get("action") {
validate_action(action, id)?;
}
if let Some(checks) = component.get("checks") {
let checks = checks
.as_array()
.ok_or_else(|| format!("component `{id}` checks must be an array"))?;
for check in checks {
let check = object(Some(check), "check")?;
reject_unknown(check, &["condition", "message"], "check")?;
let condition = check
.get("condition")
.ok_or_else(|| format!("component `{id}` check requires condition"))?;
required_string(check, "message")?;
validate_function(condition)?;
}
}
validate_dynamic_values(component)?;
Ok(())
}
fn validate_dynamic_field(
object: &Map<String, Value>,
field: &str,
literal: impl Fn(&Value) -> bool,
id: &str,
) -> Result<(), String> {
let value = object
.get(field)
.ok_or_else(|| format!("component `{id}` requires `{field}`"))?;
if literal(value) {
return Ok(());
}
let dynamic = value
.as_object()
.ok_or_else(|| format!("component `{id}` {field} has the wrong literal or dynamic type"))?;
if let Some(path) = dynamic.get("path") {
if dynamic.len() == 1 && path.is_string() {
return Ok(());
}
return Err(format!(
"component `{id}` {field} has an invalid data binding"
));
}
validate_function(value).map_err(|error| format!("component `{id}` {field}: {error}"))
}
fn validate_optional_dynamic(
object: &Map<String, Value>,
field: &str,
literal: impl Fn(&Value) -> bool,
id: &str,
) -> Result<(), String> {
if object.contains_key(field) {
validate_dynamic_field(object, field, literal, id)
} else {
Ok(())
}
}
pub(super) fn ds4_catalog() -> &'static Value {
static CATALOG: OnceLock<Value> = OnceLock::new();
CATALOG.get_or_init(|| {
serde_json::from_str(CATALOG_JSON).expect("embedded DS4Server A2UI catalog must be valid")
})
}
fn expect_string(object: &Map<String, Value>, field: &str, id: &str) -> Result<(), String> {
if !object.get(field).is_some_and(Value::is_string) {
return Err(format!("component `{id}` {field} must be a string"));
}
Ok(())
}
fn expect_number(object: &Map<String, Value>, field: &str, id: &str) -> Result<(), String> {
if !object.get(field).is_some_and(Value::is_number) {
return Err(format!("component `{id}` {field} must be a number"));
}
Ok(())
}
fn validate_enum(
object: &Map<String, Value>,
field: &str,
allowed: &[&str],
id: &str,
) -> Result<(), String> {
if let Some(value) = object.get(field) {
let value = value
.as_str()
.ok_or_else(|| format!("component `{id}` {field} must be a string"))?;
if !allowed.contains(&value) {
return Err(format!(
"component `{id}` has invalid {field} `{value}`; expected one of: {}",
allowed.join(", ")
));
}
}
Ok(())
}
fn validate_children(value: &Value, id: &str) -> Result<(), String> {
if value
.as_array()
.is_some_and(|children| children.iter().all(Value::is_string))
{
return Ok(());
}
let template = object(Some(value), "children")?;
reject_unknown(template, &["componentId", "path"], "children template")?;
required_string(template, "componentId")?;
required_string(template, "path")?;
if !required_string(template, "path")?.starts_with('/') {
return Err(format!(
"component `{id}` child template path must be a JSON Pointer"
));
}
Ok(())
}
fn validate_tabs(value: &Value, id: &str) -> Result<(), String> {
let tabs = value
.as_array()
.filter(|tabs| !tabs.is_empty())
.ok_or_else(|| format!("component `{id}` tabs must be a non-empty array"))?;
for tab in tabs {
let tab = object(Some(tab), "tab")?;
reject_unknown(tab, &["title", "child"], "tab")?;
validate_dynamic_field(tab, "title", Value::is_string, id)?;
required_string(tab, "child")?;
}
Ok(())
}
fn validate_options(value: &Value, id: &str) -> Result<(), String> {
let options = value
.as_array()
.ok_or_else(|| format!("component `{id}` options must be an array"))?;
for option in options {
let option = object(Some(option), "choice option")?;
reject_unknown(option, &["label", "value"], "choice option")?;
validate_dynamic_field(option, "label", Value::is_string, id)?;
required_string(option, "value")?;
}
Ok(())
}
fn validate_action(value: &Value, id: &str) -> Result<(), String> {
let action = object(Some(value), "action")?;
if let Some(event) = action.get("event") {
reject_unknown(action, &["event"], "action")?;
let event = object(Some(event), "action.event")?;
reject_unknown(
event,
&["name", "context", "wantResponse", "responsePath"],
"action.event",
)?;
required_string(event, "name")?;
if let Some(context) = event.get("context")
&& !context.is_object()
{
return Err(format!("component `{id}` action context must be an object"));
}
optional_bool(event, "wantResponse")?;
if let Some(path) = event.get("responsePath")
&& !path.as_str().is_some_and(|path| path.starts_with('/'))
{
return Err(format!(
"component `{id}` responsePath must be a JSON Pointer"
));
}
return Ok(());
}
reject_unknown(action, &["functionCall"], "action")?;
validate_function(
action
.get("functionCall")
.ok_or_else(|| format!("component `{id}` action requires event or functionCall"))?,
)
}
fn validate_dynamic_values(value: &Map<String, Value>) -> Result<(), String> {
for value in value.values() {
if value.get("call").is_some() {
validate_function(value)?;
}
match value {
Value::Object(object) => validate_dynamic_values(object)?,
Value::Array(values) => {
for value in values {
if let Value::Object(object) = value {
validate_dynamic_values(object)?;
}
}
}
_ => {}
}
}
Ok(())
}
pub(super) fn validate_function(value: &Value) -> Result<(), String> {
let function = object(Some(value), "function call")?;
reject_unknown(function, &["call", "args"], "function call")?;
let call = value
.get("call")
.and_then(Value::as_str)
.ok_or_else(|| "function call requires `call`".to_owned())?;
if !FUNCTIONS.contains(&call) {
return Err(format!("function `{call}` is not declared by the catalog"));
}
if value.get("args").is_some_and(|args| !args.is_object()) {
return Err(format!("function `{call}` requires object `args`"));
}
let args = value
.get("args")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let allowed: &[&str] = match call {
"required" | "email" | "formatString" | "not" => &["value"],
"regex" => &["value", "pattern"],
"length" | "numeric" => &["value", "min", "max"],
"formatNumber" => &["value", "decimals", "grouping"],
"formatCurrency" => &["value", "currency", "decimals", "grouping"],
"formatDate" => &["value", "format"],
"pluralize" => &["value", "zero", "one", "two", "few", "many", "other"],
"openUrl" => &["url"],
"and" | "or" => &["values"],
"@index" => &["offset"],
_ => &[],
};
reject_unknown(&args, allowed, &format!("function `{call}` args"))?;
let required: &[&str] = match call {
"required" | "regex" | "length" | "numeric" | "email" | "formatString" | "formatNumber"
| "not" | "pluralize" => &["value"],
"formatCurrency" => &["value", "currency"],
"formatDate" => &["value", "format"],
"openUrl" => &["url"],
"and" | "or" => &["values"],
"@index" => &[],
_ => &[],
};
if let Some(field) = required.iter().find(|field| !args.contains_key(**field)) {
return Err(format!("function `{call}` requires argument `{field}`"));
}
if call == "regex" && !args.get("pattern").is_some_and(Value::is_string) {
return Err("function `regex` requires string argument `pattern`".into());
}
if matches!(call, "length" | "numeric")
&& !args.contains_key("min")
&& !args.contains_key("max")
{
return Err(format!("function `{call}` requires `min` or `max`"));
}
if call == "pluralize" && !args.contains_key("other") {
return Err("function `pluralize` requires argument `other`".into());
}
if matches!(call, "and" | "or")
&& !args
.get("values")
.and_then(Value::as_array)
.is_some_and(|values| values.len() >= 2)
{
return Err(format!("function `{call}` requires at least two values"));
}
match call {
"regex" | "length" | "email" | "formatString" => {
validate_dynamic_field(&args, "value", Value::is_string, call)?;
}
"numeric" | "formatNumber" | "formatCurrency" | "pluralize" => {
validate_dynamic_field(&args, "value", Value::is_number, call)?;
}
"not" => validate_dynamic_field(&args, "value", Value::is_boolean, call)?,
"formatDate" => validate_dynamic_field(&args, "format", Value::is_string, call)?,
"openUrl" => {
let url = required_string(&args, "url")?;
url::Url::parse(url).map_err(|error| format!("invalid openUrl URL: {error}"))?;
}
"and" | "or" => {
for item in args["values"].as_array().unwrap() {
if !item.is_boolean() {
let item = item.as_object().ok_or_else(|| {
format!("function `{call}` values must be dynamic booleans")
})?;
if !item.contains_key("path") && !item.contains_key("call") {
return Err(format!("function `{call}` values must be dynamic booleans"));
}
}
}
}
_ => {}
}
for field in ["min", "max", "decimals", "offset"] {
if args.contains_key(field) {
validate_dynamic_field(&args, field, Value::is_number, call)?;
}
}
if args.contains_key("grouping") {
validate_dynamic_field(&args, "grouping", Value::is_boolean, call)?;
}
for field in ["currency", "zero", "one", "two", "few", "many", "other"] {
if args.contains_key(field) {
validate_dynamic_field(&args, field, Value::is_string, call)?;
}
}
Ok(())
}
+264
View File
@@ -0,0 +1,264 @@
use crate::a2ui::{BASIC_NON_MEDIA_COMPONENTS, Store, extract_lines, validate_surface_composition};
use crate::config::Config;
use crate::model::ModelChoice;
use serde_json::{Value, json};
use std::time::Duration;
const CASES: &[Case] = &[
Case {
name: "pie-natural",
prompt: "Do not call tools. The file extension counts are rs 41, md 7, and toml 2. Give me a pie chart with the numbers.",
components: &["Chart"],
chart_type: Some("pie"),
},
Case {
name: "pie-single-slice",
prompt: "Do not call tools. There are 41 rs files and no other file extensions. Show that distribution as a one-slice pie chart.",
components: &["Chart"],
chart_type: Some("pie"),
},
Case {
name: "donut-natural",
prompt: "Do not call tools. Show a donut chart for 62 completed, 23 active, and 15 blocked tasks. Include the total.",
components: &["Chart"],
chart_type: Some("donut"),
},
Case {
name: "heatmap-natural",
prompt: "Do not call tools. Show a heatmap of pull requests reviewed in 2025 and 2026, with each year as a row and January, February, and March as columns. Use values 2, 5, 3 for 2025 and 4, 1, 6 for 2026.",
components: &["Chart"],
chart_type: Some("heatmap"),
},
Case {
name: "form-controls",
prompt: "Do not call tools. Build an A2UI Form for a name, multiline notes, due date, and multiple checkbox priorities, with a submit button.",
components: &["Form"],
chart_type: None,
},
Case {
name: "filterable-choices",
prompt: "Do not call tools. Build an A2UI surface with a filterable mutually-exclusive ChoicePicker for Rust, Python, and TypeScript.",
components: &["ChoicePicker"],
chart_type: None,
},
Case {
name: "composed-basics",
prompt: "Do not call tools. Build one complete A2UI project dashboard. Compose a Card and Column containing Markdown text, an avatar Image, an Icon, a Divider, a weighted Row, Tabs, a dynamic List, a Modal, a Button, TextField, CheckBox, Slider, DateTimeInput, and a filterable mutually-exclusive ChoicePicker. Bind the controls to the data model and give the button an event.",
components: BASIC_NON_MEDIA_COMPONENTS,
chart_type: None,
},
Case {
name: "media-players",
prompt: "Do not call tools. Build one A2UI Card containing a Video with a poster URL and an AudioPlayer with a description. Use valid HTTPS media URLs and compose both into the root tree.",
components: &["Video", "AudioPlayer"],
chart_type: None,
},
];
struct Case {
name: &'static str,
prompt: &'static str,
components: &'static [&'static str],
chart_type: Option<&'static str>,
}
struct Options {
endpoint: Option<String>,
model: Option<String>,
case: Option<String>,
attempts: u32,
}
pub(crate) fn run(args: impl Iterator<Item = String>) -> Result<(), String> {
let options = parse_options(args)?;
let config = Config::load(&crate::app::config_path())?;
let endpoint = options
.endpoint
.unwrap_or_else(|| format!("http://127.0.0.1:{}", config.endpoint.port));
let model_id = options
.model
.unwrap_or_else(|| config.model.id().to_owned());
let model =
ModelChoice::from_id(&model_id).ok_or_else(|| format!("unknown model `{model_id}`"))?;
let cases = CASES
.iter()
.filter(|case| options.case.as_deref().is_none_or(|name| name == case.name))
.collect::<Vec<_>>();
if cases.is_empty() {
return Err(format!(
"unknown case; choose one of: {}",
CASES
.iter()
.map(|case| case.name)
.collect::<Vec<_>>()
.join(", ")
));
}
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(5)))
.timeout_recv_response(Some(Duration::from_secs(30 * 60)))
.timeout_recv_body(Some(Duration::from_secs(30)))
.build()
.into();
let mut system = crate::agent::system_prompt(model, &config.system_prompt, false);
system.push_str("\n\n");
system.push_str(crate::a2ui::SYSTEM_PROMPT);
let metadata = Store::default().client_metadata();
let mut passed = 0;
let total = cases.len() as u32 * options.attempts;
println!("A2UI live validation: {model_id} at {endpoint}");
for case in cases {
for attempt in 1..=options.attempts {
let user = format!("{}\n\nA2UI client metadata:\n{}", case.prompt, metadata);
let payload = json!({
"model": model_id,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user}
],
"reasoning_effort": "none",
"temperature": 0,
"max_tokens": 4096
});
let content = request(&agent, &endpoint, &payload)?;
match validate(case, &content) {
Ok(summary) => {
passed += 1;
println!("PASS {}#{attempt}: {summary}", case.name);
}
Err(error) => {
println!("FAIL {}#{attempt}: {error}", case.name);
println!(
"--- response ---\n{}\n--- end response ---",
bounded(&content)
);
}
}
}
}
println!("A2UI validation summary: {passed}/{total} passed");
if passed == total {
Ok(())
} else {
Err(format!("{} live validation case(s) failed", total - passed))
}
}
fn parse_options(mut args: impl Iterator<Item = String>) -> Result<Options, String> {
let mut options = Options {
endpoint: None,
model: None,
case: None,
attempts: 1,
};
while let Some(argument) = args.next() {
let value = match argument.as_str() {
"--endpoint" | "--model" | "--case" | "--attempts" => args
.next()
.ok_or_else(|| format!("{argument} requires a value"))?,
"--help" | "-h" => {
println!(
"Usage: ds4-server validate-a2ui [--endpoint URL] [--model ID] [--case NAME] [--attempts N]"
);
std::process::exit(0);
}
_ => return Err(format!("unknown argument `{argument}`")),
};
match argument.as_str() {
"--endpoint" => options.endpoint = Some(value.trim_end_matches('/').to_owned()),
"--model" => options.model = Some(value),
"--case" => options.case = Some(value),
"--attempts" => {
options.attempts = value
.parse::<u32>()
.ok()
.filter(|attempts| *attempts > 0)
.ok_or_else(|| "--attempts must be a positive integer".to_owned())?;
}
_ => unreachable!(),
}
}
Ok(options)
}
fn request(agent: &ureq::Agent, endpoint: &str, payload: &Value) -> Result<String, String> {
let mut response = agent
.post(&format!("{endpoint}/v1/chat/completions"))
.header("Content-Type", "application/json")
.send(payload.to_string().as_bytes())
.map_err(|error| format!("could not call the local endpoint: {error}"))?;
let body = response
.body_mut()
.read_to_string()
.map_err(|error| format!("could not read the local endpoint response: {error}"))?;
let value: Value = serde_json::from_str(&body)
.map_err(|error| format!("local endpoint returned invalid JSON: {error}"))?;
value
.pointer("/choices/0/message/content")
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| format!("local endpoint response has no assistant content: {body}"))
}
fn validate(case: &Case, content: &str) -> Result<String, String> {
let lines = extract_lines(content);
if lines.is_empty() {
return Err("the model emitted no complete fenced A2UI JSONL messages".into());
}
let mut store = Store::default();
let mut errors = Vec::new();
for (index, line) in lines.iter().enumerate() {
if let Err(error) = &line.value {
errors.push(format!("line {} is invalid JSON: {error}", index + 1));
} else if let Err(error) = store.apply_raw(&line.raw, 1) {
errors.push(format!(
"line {} failed catalog validation: {error}",
index + 1
));
}
}
if !errors.is_empty() {
return Err(errors.join("; "));
}
let surfaces = store.surfaces().collect::<Vec<_>>();
let mut reachable_components = std::collections::BTreeSet::new();
for surface in &surfaces {
reachable_components.extend(
validate_surface_composition(surface)
.map_err(|error| format!("surface `{}` is not composable: {error}", surface.id))?,
);
}
let components = surfaces
.iter()
.flat_map(|surface| surface.components.values())
.filter_map(Value::as_object)
.collect::<Vec<_>>();
for expected in case.components {
if !reachable_components.contains(*expected) {
return Err(format!(
"valid surface did not compose expected {expected} component into its root tree"
));
}
}
if let Some(expected) = case.chart_type
&& !components.iter().any(|component| {
component.get("component").and_then(Value::as_str) == Some("Chart")
&& component.get("chartType").and_then(Value::as_str) == Some(expected)
})
{
return Err(format!("valid surface omitted expected {expected} chart"));
}
Ok(format!(
"{} protocol message(s), {} surface(s), {} component(s)",
lines.len(),
surfaces.len(),
components.len()
))
}
fn bounded(content: &str) -> &str {
content
.char_indices()
.nth(8_000)
.map_or(content, |(end, _)| &content[..end])
}
+4154 -305
View File
File diff suppressed because it is too large Load Diff
+480
View File
@@ -0,0 +1,480 @@
use headless_chrome::browser::{default_executable, tab::Tab};
use headless_chrome::protocol::cdp::{Emulation, Runtime};
use headless_chrome::{Browser as Chrome, LaunchOptions};
use serde_json::Value;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
const PORT: u16 = 9333;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(3);
const CDP_TIMEOUT: Duration = Duration::from_secs(20);
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const CLICK_GOOGLE_CONSENT_JS: &str = r#"(() => {
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const pats=[/accept all/i,/i agree/i,/agree/i,/accetta tutto/i,/tout accepter/i,/aceptar todo/i,/alle akzeptieren/i];
const els=[...document.querySelectorAll('button,[role=button],input[type=submit],a')];
for (const el of els){const t=clean(el.innerText||el.value||el.textContent);if(!t)continue;if(pats.some(p=>p.test(t))){el.click();return 'clicked '+t;}}
return '';
})()"#;
const EXTRACT_SEARCH_JS: &str = r#"(() => {
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const esc=s=>clean(s).replace(/\\/g,'\\\\').replace(/\[/g,'\\[').replace(/\]/g,'\\]').replace(/\n/g,' ');
const visible=el=>{const r=el.getBoundingClientRect();const st=getComputedStyle(el);return r.width>0&&r.height>0&&st.display!=='none'&&st.visibility!=='hidden'&&st.opacity!=='0';};
const bad=h=>(/(^|\.)google\./.test(h)||/(^|\.)gstatic\./.test(h)||/(^|\.)googleusercontent\./.test(h));
const lines=['# Google search results','',`URL: ${location.href}`,'','## Visible links'];
const seen=new Set();
for(const a of document.querySelectorAll('a[href]')){if(!visible(a))continue;let href=a.href||'';try{const u=new URL(href);if(u.pathname==='/url'&&u.searchParams.get('q'))href=u.searchParams.get('q');}catch{}let u;try{u=new URL(href);}catch{continue;}if(!/^https?:$/.test(u.protocol))continue;if(bad(u.hostname))continue;const text=esc(a.innerText||a.textContent);if(text.length<3)continue;if(seen.has(u.href))continue;seen.add(u.href);lines.push(`- [${text.slice(0,180)}](${u.href})`);if(seen.size>=20)break;}
lines.push('','## Text snapshot',clean(document.body.innerText).slice(0,1200));
return lines.join('\n');
})()"#;
const EXTRACT_PAGE_JS: &str = r#"(() => {
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const esc=s=>clean(s).replace(/\\/g,'\\\\').replace(/\[/g,'\\[').replace(/\]/g,'\\]').replace(/\n/g,' ');
const visible=el=>{const r=el.getBoundingClientRect();const st=getComputedStyle(el);return r.width>0&&r.height>0&&st.display!=='none'&&st.visibility!=='hidden'&&st.opacity!=='0';};
const inline=n=>{if(!n)return'';if(n.nodeType===3)return n.nodeValue;if(n.nodeType!==1)return'';const el=n;if(el.tagName==='SCRIPT'||el.tagName==='STYLE'||el.tagName==='NOSCRIPT')return'';if(el.tagName==='A'){const t=esc(el.innerText||el.textContent);const h=el.href||'';return t&&h?`[${t}](${h})`:t;}if(el.tagName==='CODE')return '`'+clean(el.innerText||el.textContent).replace(/`/g,'\\`')+'`';return [...el.childNodes].map(inline).join('');};
const lines=[`# ${clean(document.title)||location.href}`,'',`URL: ${location.href}`,'','## Content'];
const blocks=[...document.body.querySelectorAll('h1,h2,h3,h4,h5,h6,p,li,pre,blockquote,td,th,[id="content-text"],[class*="comment-body"],[class*="comment-content"],[data-testid*="comment-text"]')];
const seen=new Set();
for(const el of blocks){if(!visible(el))continue;let s='';const tag=el.tagName;if(/^H[1-6]$/.test(tag)){s='#'.repeat(Number(tag[1]))+' '+inline(el);}else if(tag==='LI'){s='- '+inline(el);}else if(tag==='PRE'){s='```\n'+(el.innerText||el.textContent||'').trimEnd()+'\n```';}else if(tag==='BLOCKQUOTE'){s='> '+clean(el.innerText||el.textContent);}else{s=inline(el);}s=s.trim();if(!s||seen.has(s))continue;seen.add(s);lines.push('',s);if(lines.join('\n').length>900000){lines.push('','[Content truncated by browser extractor.]');break;}}
lines.push('','## Visible links');let n=0;const linkSeen=new Set();
for(const a of document.querySelectorAll('a[href]')){if(!visible(a))continue;const t=esc(a.innerText||a.textContent);if(t.length<3)continue;let u;try{u=new URL(a.href);}catch{continue;}if(!/^https?:$/.test(u.protocol)||linkSeen.has(u.href))continue;linkSeen.add(u.href);lines.push(`- [${t.slice(0,160)}](${u.href})`);if(++n>=80)break;}
return lines.join('\n');
})()"#;
const SCROLL_DYNAMIC_PAGE_JS: &str = r#"(() => new Promise(resolve => {
const root=()=>document.scrollingElement||document.documentElement||document.body;
const blockSel='h1,h2,h3,h4,h5,h6,p,li,pre,blockquote,td,th,[id="content-text"],[class*="comment-body"],[class*="comment-content"],[data-testid*="comment-text"]';
const lazySel='[onscroll],[loading="lazy"],[data-src],[data-lazy],[class*="lazy"],[class*="infinite"],[class*="virtual"],[role="feed"],[id*="comment"],[class*="comment"],[data-testid*="comment"]';
const hookCount=()=>{let n=0;try{if(window.onscroll)n++;if(document.onscroll)n++;if(document.body&&document.body.onscroll)n++;}catch(e){}try{if(typeof getEventListeners==='function'){for(const o of [window,document,document.body]){if(!o)continue;const ev=getEventListeners(o);if(ev&&ev.scroll)n+=ev.scroll.length;}}catch(e){}try{n+=document.querySelectorAll(lazySel).length;}catch(e){}return n;};
const metrics=()=>{const r=root();return {height:r?r.scrollHeight:0,view:innerHeight||900,y:scrollY||(r&&r.scrollTop)||0,text:((document.body&&document.body.innerText)||'').length,links:document.links?document.links.length:0,blocks:document.body?document.body.querySelectorAll(blockSel).length:0,hooks:hookCount()};};
const sig=m=>[m.height,m.text,m.links,m.blocks].join('|');
const grew=(a,b)=>b.height>a.height+20||b.text>a.text+200||b.links>a.links+2||b.blocks>a.blocks+2;
const scrollOnce=()=>{const r=root();if(!r)return;const h=Math.max(700,Math.floor((innerHeight||900)*0.85));window.scrollTo(0,Math.min(r.scrollHeight,(scrollY||r.scrollTop||0)+h));};
let last=metrics(),lastSig=sig(last),same=0,steps=0;
const scrollable=last.height>last.view*1.35;
if(!scrollable||last.hooks===0){resolve('scroll skipped hooks='+last.hooks+' text='+last.text);return;}
const tick=()=>{if(steps>=28){resolve('scrolled '+steps+' text='+last.text);return;}const before=last;scrollOnce();steps++;setTimeout(()=>{const now=metrics(),nowSig=sig(now);if(nowSig===lastSig)same++;else same=0;const loaded=grew(before,now);last=now;lastSig=nowSig;if(steps===1&&!loaded){resolve('scroll probe unchanged text='+now.text);return;}const atBottom=now.y+now.view+20>=now.height;if(same>=4||(atBottom&&same>=1)){resolve('scrolled '+steps+' text='+now.text);return;}tick();},900);};
tick();
}))()"#;
pub(super) struct Browser {
chrome: Option<Chrome>,
profile_dir: PathBuf,
}
impl Browser {
pub(super) fn new() -> Self {
Self {
chrome: None,
profile_dir: crate::app::browser_profile_path(),
}
}
pub(super) fn google_search(
&mut self,
query: &str,
cancel: &AtomicBool,
) -> Result<String, String> {
if query.is_empty() {
return Err("google_search requires query".into());
}
self.run_page(&google_search_url(query), EXTRACT_SEARCH_JS, false, cancel)
}
pub(super) fn visit_page(&mut self, url: &str, cancel: &AtomicBool) -> Result<String, String> {
if url.is_empty() {
return Err("visit_page requires url".into());
}
self.run_page(url, EXTRACT_PAGE_JS, true, cancel)
}
fn run_page(
&mut self,
url: &str,
extract: &str,
dynamic_scroll: bool,
cancel: &AtomicBool,
) -> Result<String, String> {
let chrome = self.ensure_browser(cancel)?.clone();
let tab = new_tab(chrome, cancel)?;
tab.set_default_timeout(CDP_TIMEOUT);
let result = (|| {
prepare_page(&tab, cancel)?;
check_cancel(cancel)?;
tab.navigate_to(url)
.map_err(|error| format!("page navigation failed: {error}"))?;
wait_navigated_ready(&tab, cancel)?;
if let Ok(clicked) = evaluate_string(&tab, CLICK_GOOGLE_CONSENT_JS, cancel)
&& !clicked.is_empty()
{
sleep(cancel, Duration::from_millis(1500))?;
wait_navigated_ready(&tab, cancel)?;
}
if dynamic_scroll {
scroll_dynamic_page(&tab, cancel)?;
}
evaluate_string(&tab, extract, cancel)
})();
close_tab(tab, cancel.load(Ordering::Relaxed));
result
}
fn ensure_browser(&mut self, cancel: &AtomicBool) -> Result<&Chrome, String> {
if self.chrome.is_none() {
let profile_dir = self.profile_dir.clone();
self.chrome = Some(background(cancel, move || start_browser(profile_dir))?);
}
Ok(self.chrome.as_ref().unwrap())
}
}
fn start_browser(profile_dir: PathBuf) -> Result<Chrome, String> {
if let Some(browser) = connect_existing() {
return Ok(browser);
}
if profile_dir.to_str().is_none() {
return Err("The browser profile path is not valid UTF-8.".into());
}
std::fs::create_dir_all(&profile_dir)
.map_err(|error| format!("failed to create Chrome profile dir: {error}"))?;
let executable = std::env::var_os("DS4_CHROME")
.filter(|path| !path.is_empty())
.map(PathBuf::from)
.map_or_else(default_executable, Ok)
.map_err(|error| format!("could not find Chrome: {error}"))?;
let args = [
OsStr::new("--remote-allow-origins=*"),
OsStr::new("--no-default-browser-check"),
OsStr::new("--disable-sync"),
OsStr::new("--use-mock-keychain"),
OsStr::new("--password-store=basic"),
OsStr::new("--mute-audio"),
OsStr::new("about:blank"),
];
let options = LaunchOptions::default_builder()
.headless(true)
.path(Some(executable))
.user_data_dir(Some(profile_dir))
.port(Some(PORT))
.idle_browser_timeout(CDP_TIMEOUT)
.ignore_certificate_errors(false)
.enable_gpu(true)
.disable_default_args(true)
.args(args.to_vec())
.build()
.map_err(|error| format!("could not configure Chrome: {error}"))?;
Chrome::new(options).map_err(|error| format!("could not start Chrome: {error}"))
}
fn connect_existing() -> Option<Chrome> {
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(CONNECT_TIMEOUT))
.timeout_recv_response(Some(CONNECT_TIMEOUT))
.timeout_recv_body(Some(CONNECT_TIMEOUT))
.build()
.into();
let mut response = agent
.get(&format!("http://127.0.0.1:{PORT}/json/version"))
.call()
.ok()?;
let body = response.body_mut().read_to_string().ok()?;
let websocket = serde_json::from_str::<Value>(&body)
.ok()?
.get("webSocketDebuggerUrl")?
.as_str()?
.to_owned();
Chrome::connect_with_timeout(websocket, CDP_TIMEOUT).ok()
}
fn new_tab(chrome: Chrome, cancel: &AtomicBool) -> Result<Arc<Tab>, String> {
let (sender, receiver) = mpsc::sync_channel(1);
thread::spawn(move || {
let result = chrome
.new_tab()
.map_err(|error| format!("could not open browser tab: {error}"));
if let Err(mpsc::SendError(Ok(tab))) = sender.send(result) {
let _ = tab.close_target();
}
});
wait(receiver, cancel)
}
fn prepare_page(tab: &Arc<Tab>, cancel: &AtomicBool) -> Result<(), String> {
check_cancel(cancel)?;
tab.call_method(Runtime::Enable(None))
.map_err(|error| format!("could not enable page runtime: {error}"))?;
let _ = tab.call_method(Emulation::SetFocusEmulationEnabled { enabled: true });
let _ = tab.call_method(Emulation::SetDeviceMetricsOverride {
width: 1365,
height: 900,
device_scale_factor: 1.0,
mobile: false,
scale: None,
screen_width: None,
screen_height: None,
position_x: None,
position_y: None,
dont_set_visible_size: None,
screen_orientation: None,
viewport: None,
display_feature: None,
device_posture: None,
});
wait_ready(tab, cancel)
}
fn wait_ready(tab: &Arc<Tab>, cancel: &AtomicBool) -> Result<(), String> {
for _ in 0..80 {
check_cancel(cancel)?;
if let Ok(state) = evaluate_string(tab, "document.readyState", cancel)
&& matches!(state.as_str(), "complete" | "interactive")
{
return sleep(cancel, Duration::from_millis(800));
}
sleep(cancel, Duration::from_millis(250))?;
}
Ok(())
}
fn wait_navigated_ready(tab: &Arc<Tab>, cancel: &AtomicBool) -> Result<(), String> {
let mut last_len = -1;
let mut stable = 0;
let mut saw_real_url = false;
for iteration in 0..100 {
check_cancel(cancel)?;
let Ok(probe) = evaluate_string(
tab,
"location.href+'\\n'+document.readyState+'\\n'+((document.body&&document.body.innerText)||'').length",
cancel,
) else {
sleep(cancel, Duration::from_millis(250))?;
continue;
};
let Ok((href, ready, text_len)) = parse_probe(&probe) else {
sleep(cancel, Duration::from_millis(250))?;
continue;
};
let real_url = !href.is_empty() && href != "about:blank" && !href.starts_with("chrome://");
let ready = matches!(ready, "complete" | "interactive");
saw_real_url |= real_url;
if text_len > 0 && text_len == last_len {
stable += 1;
} else {
stable = 0;
}
last_len = text_len;
if saw_real_url && ready && text_len > 0 && stable >= 2 {
return sleep(cancel, Duration::from_millis(500));
}
if saw_real_url && ready && iteration >= 24 {
return Ok(());
}
sleep(cancel, Duration::from_millis(250))?;
}
Ok(())
}
fn scroll_dynamic_page(tab: &Arc<Tab>, cancel: &AtomicBool) -> Result<(), String> {
check_cancel(cancel)?;
let result = evaluate_string(tab, SCROLL_DYNAMIC_PAGE_JS, cancel);
check_cancel(cancel)?;
match result {
Err(error) if error == "interrupted" => Err(error),
_ => Ok(()),
}
}
fn evaluate_string(
tab: &Arc<Tab>,
expression: &str,
cancel: &AtomicBool,
) -> Result<String, String> {
let tab = Arc::clone(tab);
let expression = expression.to_owned();
background(cancel, move || {
let response = tab
.call_method(Runtime::Evaluate {
expression,
object_group: None,
include_command_line_api: Some(true),
silent: None,
context_id: None,
return_by_value: Some(true),
generate_preview: None,
user_gesture: None,
await_promise: Some(true),
throw_on_side_effect: None,
timeout: None,
disable_breaks: None,
repl_mode: None,
allow_unsafe_eval_blocked_by_csp: None,
unique_context_id: None,
serialization_options: None,
})
.map_err(|error| format!("JavaScript evaluation failed: {error}"))?;
if response.exception_details.is_some() {
return Err("JavaScript evaluation failed".into());
}
response
.result
.value
.and_then(|value| value.as_str().map(str::to_owned))
.ok_or_else(|| "Runtime.evaluate did not return a string".into())
})
}
fn background<T, F>(cancel: &AtomicBool, operation: F) -> Result<T, String>
where
T: Send + 'static,
F: FnOnce() -> Result<T, String> + Send + 'static,
{
let (sender, receiver) = mpsc::sync_channel(1);
thread::spawn(move || {
let _ = sender.send(operation());
});
wait(receiver, cancel)
}
fn wait<T>(receiver: mpsc::Receiver<Result<T, String>>, cancel: &AtomicBool) -> Result<T, String> {
loop {
match receiver.recv_timeout(POLL_INTERVAL) {
Ok(result) => return result,
Err(mpsc::RecvTimeoutError::Timeout) => check_cancel(cancel)?,
Err(mpsc::RecvTimeoutError::Disconnected) => {
return Err("browser worker stopped unexpectedly".into());
}
}
}
}
fn close_tab(tab: Arc<Tab>, background: bool) {
if background {
thread::spawn(move || {
let _ = tab.close_target();
});
} else {
let _ = tab.close_target();
}
}
fn check_cancel(cancel: &AtomicBool) -> Result<(), String> {
if cancel.load(Ordering::Relaxed) {
Err("interrupted".into())
} else {
Ok(())
}
}
fn sleep(cancel: &AtomicBool, duration: Duration) -> Result<(), String> {
let mut remaining = duration;
while !remaining.is_zero() {
check_cancel(cancel)?;
let step = remaining.min(POLL_INTERVAL);
thread::sleep(step);
remaining = remaining.saturating_sub(step);
}
check_cancel(cancel)
}
fn parse_probe(probe: &str) -> Result<(&str, &str, i64), String> {
let mut parts = probe.splitn(3, '\n');
let href = parts.next().unwrap_or_default();
let ready = parts
.next()
.ok_or_else(|| "page readiness probe returned malformed data".to_owned())?;
let text_len = parts
.next()
.ok_or_else(|| "page readiness probe returned malformed data".to_owned())?
.parse()
.unwrap_or(0);
Ok((href, ready, text_len))
}
fn google_search_url(query: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut url = String::from("https://www.google.com/search?q=");
for byte in query.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
url.push(char::from(byte));
} else {
url.push('%');
url.push(char::from(HEX[(byte >> 4) as usize]));
url.push(char::from(HEX[(byte & 15) as usize]));
}
}
url
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
use std::net::TcpListener;
#[test]
fn browser_helpers_preserve_ds4_wire_values() {
assert_eq!(
google_search_url("café & Rust~"),
"https://www.google.com/search?q=caf%C3%A9%20%26%20Rust~"
);
assert_eq!(
parse_probe("https://example.com\ncomplete\n42").unwrap(),
("https://example.com", "complete", 42)
);
assert!(parse_probe("incomplete").is_err());
}
#[test]
#[ignore = "requires an installed Chrome browser"]
fn headless_browser_extracts_a_local_page() {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let address = listener.local_addr().unwrap();
let server = thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0; 4096];
let _ = stream.read(&mut request);
let body = "<title>Fixture</title><h1>Hello</h1><p>Browser works.</p>";
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.unwrap();
});
let profile_dir = std::env::temp_dir().join(format!(
"ds4-browser-test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let mut browser = Browser {
chrome: None,
profile_dir: profile_dir.clone(),
};
let output = browser
.run_page(
&format!("http://{address}"),
EXTRACT_PAGE_JS,
false,
&AtomicBool::new(false),
)
.unwrap();
assert!(output.contains("# Fixture"));
assert!(output.contains("# Hello"));
assert!(output.contains("Browser works."));
drop(browser);
server.join().unwrap();
std::fs::remove_dir_all(profile_dir).unwrap();
}
}
+2399 -358
View File
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
use super::*;
impl App {
pub(super) fn install_extension(&mut self) {
let source = self.extension_source.trim().to_owned();
let requested_ref = self.extension_ref.trim().to_owned();
if source.is_empty() {
self.extension_error = Some("Enter an HTTPS Git repository URL.".into());
return;
}
self.cancel_running_extension_hooks();
let root = extensions_path();
self.start_extension_operation("Installing extension", move || {
crate::extensions::ExtensionRegistry::install(
&root,
&source,
(!requested_ref.is_empty()).then_some(requested_ref.as_str()),
)
});
}
pub(super) fn update_extension(&mut self, id: String) {
self.cancel_running_extension_hooks();
let root = extensions_path();
self.start_extension_operation("Updating extension", move || {
crate::extensions::ExtensionRegistry::update(&root, &id)
});
}
pub(super) fn toggle_extension(&mut self, id: String, enabled: bool) {
let Some(extension) = self
.extensions
.extensions
.iter()
.find(|extension| extension.id == id)
else {
return;
};
if enabled && extension.has_commands() && !extension.trusted {
self.pending_extension_trust = Some(id);
return;
}
self.cancel_running_extension_hooks();
let root = extensions_path();
self.start_extension_operation(
if enabled {
"Enabling extension"
} else {
"Disabling extension"
},
move || crate::extensions::ExtensionRegistry::set_enabled(&root, &id, enabled),
);
}
pub(super) fn confirm_extension_trust(&mut self) {
let Some(id) = self.pending_extension_trust.take() else {
return;
};
self.cancel_running_extension_hooks();
let root = extensions_path();
self.start_extension_operation("Enabling trusted extension", move || {
crate::extensions::ExtensionRegistry::trust_and_enable(&root, &id)
});
}
pub(super) fn confirm_uninstall_extension(&mut self) {
let Some(id) = self.pending_extension_uninstall.take() else {
return;
};
self.cancel_running_extension_hooks();
let root = extensions_path();
self.start_extension_operation("Uninstalling extension", move || {
crate::extensions::ExtensionRegistry::uninstall(&root, &id)
});
}
fn start_extension_operation(
&mut self,
label: &str,
operation: impl FnOnce() -> Result<crate::extensions::ExtensionRegistry, String>
+ Send
+ 'static,
) {
if self.extension_operation.is_some() {
self.extension_error = Some("Another extension operation is already running.".into());
return;
}
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let _ = sender.send(operation());
});
self.extension_operation = Some(ActiveExtensionOperation {
label: label.into(),
receiver,
});
self.extension_error = None;
}
pub(super) fn poll_extension_operation(&mut self) {
let Some(operation) = &self.extension_operation else {
return;
};
match operation.receiver.try_recv() {
Ok(Ok(extensions)) => {
self.extensions = extensions;
self.extension_operation = None;
self.extension_source.clear();
self.extension_ref.clear();
self.extension_error = None;
#[cfg(target_os = "macos")]
{
self.agent_tools = None;
}
}
Ok(Err(error)) => {
self.extension_operation = None;
self.extension_error = Some(error);
}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
self.extension_operation = None;
self.extension_error = Some("The extension operation stopped unexpectedly.".into());
}
}
}
pub(super) fn extension_operation_label(&self) -> Option<&str> {
self.extension_operation
.as_ref()
.map(|operation| operation.label.as_str())
}
pub(super) fn cancel_running_extension_hooks(&mut self) {
#[cfg(target_os = "macos")]
{
let requests = self
.active_extension_hooks
.iter()
.chain(
self.background_chats
.values()
.filter_map(|chat| chat.active_extension_hooks.as_ref()),
)
.map(|request| (Arc::clone(&request.cancel), Arc::clone(&request.finished)))
.collect::<Vec<_>>();
for (cancel, _) in &requests {
cancel.store(true, Ordering::Relaxed);
}
let deadline = Instant::now() + Duration::from_secs(1);
while requests
.iter()
.any(|(_, finished)| !finished.load(Ordering::Acquire))
&& Instant::now() < deadline
{
thread::sleep(Duration::from_millis(10));
}
}
}
}
+1679 -142
View File
File diff suppressed because it is too large Load Diff
+1172
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More