Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/.github/CODEOWNERS @leehack
/tools/build.py @leehack
/tools/validate_artifacts.py @leehack
/tools/package_release.py @leehack
/tools/prepared_release.py @leehack
/tools/vulkan_tri.py @leehack
/tools/runtime_archive.py @leehack
/tests/test_vulkan_tri*.py @leehack
/tests/fixtures/vulkan/ @leehack
/docs/vulkan_tri_guard.md @leehack
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Vulkan variants link the Vulkan loader (`libvulkan.so.1`, `vulkan-1.dll`) and
fail to load without it; consumers fall back to the CPU variant. CI checks them
on Mesa lavapipe only, so GPU performance still needs hardware runs.

Affected GLSL compilers can compile `tri_f16`'s integer zero constructor to
NaN. Vulkan builds, packaging and prepared-artifact replay fail unless the
actual embedded triangle module has a recognized layout and its rejected
branch stores positive zero. This validates the exact artifact; the upstream
source and compiler remain unchanged. See [the shader guard](docs/vulkan_tri_guard.md).

Android GPU backends are not shipped. On tested devices, Vulkan crashed in
the Adreno 750 driver's shader compiler and on a null `vkGetBufferDeviceAddress`
on Mali-G68, and OpenCL on Adreno 750 was slower than the CPU.
Expand Down
44 changes: 44 additions & 0 deletions docs/vulkan_tri_guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Vulkan triangle artifact guard

Some GLSL compilers turn GGML's `tri.comp` `D_TYPE(0)` into a NaN in the
`tri_f16` module. Two builds of identical sources can differ. The owner source
and upstream submodule remain unchanged; compilation is allowed to finish,
then the exact generated and embedded bytes must pass the guard before release.
A rejected build is not qualified by a passing retry or by another target.

`tools/vulkan_tri.py` admits the reviewed 6352-byte and 6388-byte module
layouts. Identification normalizes only a float32 constant narrowed to half;
validation examines the original bytes. It requires one triangle mode switch,
the accepted branch's input-to-output copy, and the rejected branch's float32
positive zero converted to half and stored to output binding 1. NaN, finite
nonzero, negative zero, missing or ambiguous modules, changed layouts and a
missing output data path fail closed. This is a narrow proof for this operation,
not a general shader validator or GPU execution test.

New builds capture the actual generated `tri_f16.spv` in `build-info.json` as
`vulkanTriF16` schema 1, with its SHA-256 and hex bytes. Build, runtime validation
and packaging require this capture to match the unique module in the runtime.
Package verification and publication inspect the actual library inside every
Vulkan runtime archive again, independently of accelerator flags supplied by
that archive. Existing source, manifest, archive digest and target-inventory
checks remain in force. Runtime archives reject noncanonical names, normalized
or case-fold aliases, links and nonregular members before any byte selection.
Canonical directory entries remain supported, so ordinary extraction cannot replace a validated library through another name.

Prepared artifacts from before this field existed use an explicit legacy path:
the verifier identifies and proves the same unique known module directly in
the archive's runtime bytes. A present null, malformed or unsupported-schema
field is rejected. The legacy path supplies no invented build capture or owner
commit; its trusted prepared-run source and archive hashes remain unchanged.
It has the same module identity and positive-zero proof as new artifacts.

A compiler or upstream update that changes the layout needs a fresh independent
source/module review and negative controls before adding an identity. Do not
refresh fingerprints merely to make a failed candidate pass. A source/compiler
workaround can be carried only under `patches/README.md`, including its required
upstream retirement link; this guard does not claim such a fix has landed.

`tests/test_vulkan_tri.py` covers module, flow, layout and metadata failures.
`tests/test_vulkan_tri_packaging.py` exercises the real build, artifact validator,
package writer and prepared replay entry points using deterministic compiled
fixtures. Heavy device and model qualification remains separate.
11 changes: 11 additions & 0 deletions tests/fixtures/vulkan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
`tri_f16.spv` is the 6352-byte compiled positive-zero module from GGML
`a5915eb0e47adef21c3f25b889a008925ea838c2`, with `tri.comp`'s `D_TYPE(0)`
replaced by `D_TYPE(0.0)`. SHA-256:
`a8691344947943aa581022ecfc95953c02ea3247f85abd0d2f46a23ad49cbe0f`.

Reproduce with shaderc/glslc using `-fshader-stage=compute
--target-env=vulkan1.2 -O -DA_TYPE=float16_t -DD_TYPE=float16_t`, including
GGML's `vulkan-shaders` directory. The regression suite compiles the actual
pinned source when glslc is available. Deterministic mutations replace its
rejected-branch float32 zero constant with NaN/nonzero or remove its use;
they exercise the validator on every platform without a GPU or SDK.
Binary file added tests/fixtures/vulkan/tri_f16.spv
Binary file not shown.
13 changes: 12 additions & 1 deletion tests/test_prepared_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import copy
import io
import hashlib
import json
import os
import stat
Expand Down Expand Up @@ -37,7 +38,17 @@ def setUp(self):
name = f"{release.PACKAGE}-{kind}-{target}-{TAG}.tar.gz"
path = self.dist / name
with tarfile.open(path, "w:gz") as archive:
info = json.dumps({"target": target, "upstream": self.manifest["upstream"]}).encode()
build_info = {"target": target, "upstream": self.manifest["upstream"]}
if kind == 'runtime' and 'vulkan' in release.TARGETS[target].accelerators:
module = Path(__file__).parent / 'fixtures/vulkan/tri_f16.spv'
payload = module.read_bytes()
build_info['vulkanTriF16'] = {'schemaVersion': 1,
'sha256': hashlib.sha256(payload).hexdigest(),
'spirv_hex': payload.hex()}
library = tarfile.TarInfo('lib/' + release.TARGETS[target].library)
library.size = len(payload)
archive.addfile(library, io.BytesIO(payload))
info = json.dumps(build_info).encode()
entry = tarfile.TarInfo("build-info.json")
entry.size = len(info)
archive.addfile(entry, io.BytesIO(info))
Expand Down
9 changes: 9 additions & 0 deletions tests/test_release_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ def test_linux_runtime_builds_keep_the_qualified_ubuntu_baseline(self):
rf"target: {target}, runs_on: ubuntu-24\.04,",
)

def test_release_byte_guard_sources_have_owner_coverage(self):
owners = (ROOT / '.github/CODEOWNERS').read_text().splitlines()
for path in ('tools/build.py', 'tools/validate_artifacts.py',
'tools/package_release.py', 'tools/prepared_release.py',
'tools/vulkan_tri.py', 'tools/runtime_archive.py'):
with self.subTest(path=path):
self.assertTrue((ROOT / path).is_file())
self.assertIn('/' + path + ' @leehack', owners)

def test_publication_requires_explicit_opt_in(self):
workflow = (ROOT / ".github/workflows/native_release.yml").read_text()
self.assertIn(
Expand Down
112 changes: 112 additions & 0 deletions tests/test_vulkan_tri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Compiled triangle zero-store, source staging and packaged-byte regressions."""
from __future__ import annotations

from pathlib import Path
import shutil
import struct
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'tools'))
from vulkan_tri import GENERATED_MODULE, build_record, validate_artifact, validate_module, validate_artifact_bytes

FIXTURE = ROOT / 'tests/fixtures/vulkan/tri_f16.spv'
GGML = ROOT / 'third_party/stable-diffusion.cpp/ggml'
TRI = 'src/ggml-vulkan/vulkan-shaders/tri.comp'


def mutate_constant(value: int) -> bytes:
data = bytearray(FIXTURE.read_bytes())
at = 20
while at < len(data):
word = struct.unpack_from('<I', data, at)[0]
if word & 65535 == 43:
type_id, result_id = struct.unpack_from('<II', data, at + 4)
if result_id == 466:
struct.pack_into('<I', data, at + 12, value)
return bytes(data)
at += (word >> 16) * 4
raise AssertionError('zero constant not found')


class VulkanTriangleTest(unittest.TestCase):
def test_positive_zero_fixture(self):
validate_module(FIXTURE.read_bytes())

def test_nan_and_finite_nonzero_and_negative_zero_are_rejected(self):
for value in [0xfffffe00, 0x3f800000, 0x80000000]:
with self.subTest(value=hex(value)), self.assertRaisesRegex(ValueError, 'positive zero'):
validate_module(mutate_constant(value))

def test_missing_conversion_use_is_rejected(self):
data = bytearray(FIXTURE.read_bytes())
at = 20
while at < len(data):
word = struct.unpack_from('<I', data, at)[0]
if word & 65535 == 115 and struct.unpack_from('<I', data, at + 12)[0] == 466:
struct.pack_into('<I', data, at + 12, 447)
break
at += (word >> 16) * 4
with self.assertRaisesRegex(ValueError, 'output branches'):
validate_module(bytes(data))

def test_malformed_or_truncated_module_is_rejected(self):
for data in [b'', FIXTURE.read_bytes()[:-4], b'bad!' + FIXTURE.read_bytes()[4:]]:
with self.subTest(size=len(data)), self.assertRaises(ValueError):
validate_module(data)

def test_compiled_module_must_be_unique_in_packaged_library(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
(root / GENERATED_MODULE).parent.mkdir(parents=True)
shutil.copy2(FIXTURE, root / GENERATED_MODULE)
record = build_record(root)
library = root / 'runtime'
library.write_bytes(b'prefix' + FIXTURE.read_bytes() + b'suffix')
validate_artifact(library, record)
for data in [b'none', FIXTURE.read_bytes() * 2, mutate_constant(0xfffffe00)]:
library.write_bytes(data)
with self.assertRaises(ValueError):
validate_artifact(library, record)
library.write_bytes(FIXTURE.read_bytes())
record['sha256'] = '0' * 64
with self.assertRaisesRegex(ValueError, 'digest'):
validate_artifact(library, record)

def test_only_configured_generated_module_is_accepted(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
shutil.copy2(FIXTURE, root / 'tri_f16.spv')
with self.assertRaisesRegex(ValueError, 'generated module is missing'):
build_record(root)
(root / GENERATED_MODULE).parent.mkdir(parents=True)
shutil.copy2(FIXTURE, root / GENERATED_MODULE)
build_record(root)

def test_present_bad_metadata_is_not_legacy(self):
data = FIXTURE.read_bytes()
validate_artifact_bytes(data, None, allow_legacy=True)
for record in [{}, {'schemaVersion': 2}, {'schemaVersion': 1, 'sha256': '0' * 64, 'spirv_hex': data.hex()}]:
with self.subTest(record=record), self.assertRaises(ValueError):
validate_artifact_bytes(data, record, allow_legacy=True)

def test_legacy_still_rejects_bad_or_ambiguous_actual_module(self):
for data in [b'none', mutate_constant(0xfffffe00), mutate_constant(0x3f800000),
FIXTURE.read_bytes() + mutate_constant(0xfffffe00)]:
with self.subTest(size=len(data)), self.assertRaises(ValueError):
validate_artifact_bytes(data, None, allow_legacy=True)

def test_changed_layout_requires_review(self):
data = bytearray(FIXTURE.read_bytes())
# Generator ID does not change the flow proof; layout admission remains explicit.
struct.pack_into('<I', data, 8, 0xabcdef)
with self.assertRaisesRegex(ValueError, 'unrecognized module layout'):
validate_module(bytes(data))
with self.assertRaisesRegex(ValueError, 'unrecognized'):
validate_artifact_bytes(bytes(data), None, allow_legacy=True)


if __name__ == '__main__':
unittest.main()
Loading
Loading