diff --git a/CMakeLists.txt b/CMakeLists.txt index b3b97b7d..d3f19e30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,16 @@ enable_testing() # one. JSON and the HTTP server/client are pinned here, at the commits the # engine used before (nlohmann/json v3.11.3, cpp-httplib v0.47.0). include(FetchContent) +if(WIN32) + # Windows 10 or later: cpp-httplib's floor; the headers leave out min/max macros + add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00 NOMINMAX WIN32_LEAN_AND_MEAN) + # MinGW's headers lack GetAddrInfoExCancel; the engine only ever connects to 127.0.0.1 + set(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO OFF CACHE BOOL "" FORCE) + if(MINGW) + # one self-contained 1bit.exe: the C++ runtime linked in, not beside it as DLLs + add_link_options(-static) + endif() +endif() find_package(Threads REQUIRED) set(JSON_BuildTests OFF CACHE INTERNAL "") FetchContent_Declare(json @@ -213,7 +223,7 @@ option(ONEBIT_NPU_HOST "Build the NPU host library and its tests" ${_onebit_npu_ if(ONEBIT_NPU_HOST) find_package(PkgConfig REQUIRED) pkg_check_modules(PCRE2 REQUIRED IMPORTED_TARGET libpcre2-8) -add_library(onebit_npu STATIC npu/full_elf.cpp npu/model.cpp npu/pack.cpp npu/private_route.cpp npu/q4nx.cpp +add_library(onebit_npu STATIC npu/file_map.cpp npu/full_elf.cpp npu/model.cpp npu/pack.cpp npu/private_route.cpp npu/q4nx.cpp npu/tokenizer.cpp) target_include_directories(onebit_npu PUBLIC npu) target_link_libraries(onebit_npu PUBLIC nlohmann_json::nlohmann_json PRIVATE PkgConfig::PCRE2) diff --git a/app/main.cpp b/app/main.cpp index 86b115a2..3f83a7a8 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -51,9 +51,13 @@ #include #include +#ifdef _WIN32 +#include +#else #include #include #include +#endif namespace { @@ -140,6 +144,33 @@ int run_npu(int argc, char** argv) { // then comfyui_cpp on PATH. It is opened once, checked through that descriptor (a regular, // executable file that other users cannot write) and run with fexecve, so the file that was // checked is the file that runs. +#ifdef _WIN32 +// Windows: $ONEBIT_COMFYUI (an absolute path), then this build's, then comfyui_cpp.exe on PATH; +// run as a child that 1bit waits for (no exec in place on Windows). +int run_comfy(int argc, char** argv) { + if (argc < 1 || !std::strcmp(argv[0], "-h") || !std::strcmp(argv[0], "--help")) { + std::printf("usage: 1bit comfy \n" + " runs a ComfyUI API-format workflow (SD1.5 txt2img/img2img, see docs/comfyui.md)\n"); + return argc < 1 ? 2 : 0; + } + std::string bin; + if (const char* e = std::getenv("ONEBIT_COMFYUI"); e && *e) { + if (!std::filesystem::path(e).is_absolute()) + throw std::runtime_error("ONEBIT_COMFYUI must be an absolute path, not " + std::string(e)); + bin = e; + } +#ifdef ONEBIT_COMFYUI_BIN + if (bin.empty() && std::filesystem::exists(ONEBIT_COMFYUI_BIN)) bin = ONEBIT_COMFYUI_BIN; +#endif + if (bin.empty()) bin = "comfyui_cpp.exe"; // _spawnvp searches PATH + std::vector args{bin.c_str()}; + for (int i = 0; i < argc; ++i) args.push_back(argv[i]); + args.push_back(nullptr); + const intptr_t rc = ::_spawnvp(_P_WAIT, bin.c_str(), args.data()); + if (rc < 0) throw std::runtime_error("cannot run " + bin + ": " + std::strerror(errno)); + return int(rc); +} +#else std::string find_comfy() { if (const char* e = std::getenv("ONEBIT_COMFYUI"); e && *e) { if (e[0] != '/') throw std::runtime_error("ONEBIT_COMFYUI must be an absolute path, not " + std::string(e)); @@ -187,6 +218,7 @@ int run_comfy(int argc, char** argv) { ::close(fd); throw std::runtime_error("cannot run " + bin + ": " + std::strerror(err)); } +#endif #ifdef ONEBIT_LAYA // One request in, one device out: runs the Laya scorer's fixed routing question // against the request state and prints the winning device (npu|hrx|vulkan|zinc). diff --git a/app/serve.cpp b/app/serve.cpp index e27b332f..e4e941ef 100644 --- a/app/serve.cpp +++ b/app/serve.cpp @@ -73,21 +73,29 @@ #include #include #include -#if defined(__linux__) -#include -#else -#include -#endif #include #include +#include +#include +#if defined(_WIN32) +// httplib.h brought in winsock2.h and windows.h +#elif defined(__linux__) +#include +#include #include #include +#include +#else #include -#include +#include +#include +#include #include -#include +#endif +#ifndef _WIN32 extern char** environ; +#endif namespace onebit { @@ -185,23 +193,106 @@ std::string default_mlx() { } int free_port() { +#ifdef _WIN32 + // Winsock is started by httplib.h (its WSInit) + const SOCKET s = ::socket(AF_INET, SOCK_STREAM, 0); + const bool bad_socket = s == INVALID_SOCKET; + auto close_socket = [&] { ::closesocket(s); }; + int len = sizeof(sockaddr_in); +#else const int s = ::socket(AF_INET, SOCK_STREAM, 0); + const bool bad_socket = s < 0; + auto close_socket = [&] { ::close(s); }; + socklen_t len = sizeof(sockaddr_in); +#endif sockaddr_in a{}; a.sin_family = AF_INET; a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); a.sin_port = 0; - socklen_t len = sizeof(a); - if (s < 0 || ::bind(s, reinterpret_cast(&a), sizeof(a)) != 0 || + if (bad_socket || ::bind(s, reinterpret_cast(&a), sizeof(a)) != 0 || ::getsockname(s, reinterpret_cast(&a), &len) != 0) { - if (s >= 0) ::close(s); + if (!bad_socket) close_socket(); throw std::runtime_error("cannot find a free loopback port"); } const int port = ntohs(a.sin_port); - ::close(s); + close_socket(); return port; } // A child OpenAI-compatible server on a loopback port. +#ifdef _WIN32 +// Windows: CreateProcess into a job object that kills its processes when its last handle +// closes, so the backend dies with `1bit serve` for any reason (Linux: PR_SET_PDEATHSIG). +class Child { +public: + Child(const std::vector& argv, const std::vector& env_extra, int port) + : port_(port) { + // the command line, each argument quoted the way CommandLineToArgvW reads it back + std::string cmd; + for (const auto& a : argv) { + if (!cmd.empty()) cmd += ' '; + cmd += '"'; + size_t bs = 0; + for (char ch : a) { + if (ch == '\\') { bs++; continue; } + if (ch == '"') cmd.append(bs * 2 + 1, '\\'); + else cmd.append(bs, '\\'); + bs = 0; + cmd += ch; + } + cmd.append(bs * 2, '\\'); + cmd += '"'; + } + // our environment plus env_extra (a value the user set wins), as a double-NUL block + std::vector env_store; + if (char* block = ::GetEnvironmentStringsA()) { + for (char* e = block; *e; e += std::strlen(e) + 1) env_store.emplace_back(e); + ::FreeEnvironmentStringsA(block); + } + for (const auto& e : env_extra) { + const std::string key = e.substr(0, e.find('=') + 1); + bool present = false; + for (const auto& have : env_store) present = present || have.rfind(key, 0) == 0; + if (!present) env_store.push_back(e); + } + std::string env_block; + for (const auto& e : env_store) { env_block += e; env_block += '\0'; } + env_block += '\0'; + job_ = ::CreateJobObjectA(nullptr, nullptr); + if (job_) { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION li{}; + li.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + ::SetInformationJobObject(job_, JobObjectExtendedLimitInformation, &li, sizeof li); + } + STARTUPINFOA si{}; + si.cb = sizeof si; + PROCESS_INFORMATION pi{}; + if (!::CreateProcessA(nullptr, cmd.data(), nullptr, nullptr, FALSE, CREATE_SUSPENDED, env_block.data(), nullptr, + &si, &pi)) + throw std::runtime_error("cannot start " + argv[0] + " (error " + std::to_string(::GetLastError()) + ")"); + if (job_) ::AssignProcessToJobObject(job_, pi.hProcess); // before it runs: its children join too + ::ResumeThread(pi.hThread); + ::CloseHandle(pi.hThread); + proc_ = pi.hProcess; + } + ~Child() { + stop(); + if (job_) ::CloseHandle(job_); + } + Child(const Child&) = delete; + Child& operator=(const Child&) = delete; + + void stop() { + if (!proc_) return; + ::TerminateProcess(proc_, 1); // no SIGTERM on Windows; llama-server keeps no state to flush + ::WaitForSingleObject(proc_, 5000); + ::CloseHandle(proc_); + proc_ = nullptr; + } + + bool alive() const { return proc_ && ::WaitForSingleObject(proc_, 0) == WAIT_TIMEOUT; } + +#else class Child { public: Child(const std::vector& argv, const std::vector& env_extra, int port) @@ -256,6 +347,7 @@ class Child { } bool alive() const { return pid_ > 0 && ::waitpid(pid_, nullptr, WNOHANG) == 0; } +#endif // Polls the child's /health until it answers 200, it exits, or time runs out. bool wait_ready(std::chrono::seconds timeout, const std::atomic& stop) const { @@ -273,7 +365,11 @@ class Child { int port() const { return port_; } private: +#ifdef _WIN32 + HANDLE proc_ = nullptr, job_ = nullptr; +#else pid_t pid_ = -1; +#endif int port_; }; @@ -736,10 +832,15 @@ int serve_child(const Options& o) { ~Stop() { s.stop(); if (t.joinable()) t.join(); } } stop{srv, listener}; +#ifdef _WIN32 + std::signal(SIGTERM, on_stop_signal); + std::signal(SIGINT, on_stop_signal); +#else struct sigaction sa{}; sa.sa_handler = on_stop_signal; ::sigaction(SIGTERM, &sa, nullptr); ::sigaction(SIGINT, &sa, nullptr); +#endif for (const auto& b : laya ? std::vector{} : backends) { std::fprintf(stderr, "1bit serve: %s on %s (%s)\n", id.c_str(), b.device.c_str(), b.argv[0].c_str()); children.push_back(std::make_unique(b.argv, b.env, b.port)); diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 00000000..c12b7b09 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,66 @@ + + +# Windows + +`1bit.exe` runs on Windows 10 and later (x64): `1bit serve` with the Vulkan backend, `1bit route` (Laya) and `1bit comfy`. It is cross-built on Linux. There is no NPU lane, HRX, ROCm or ZINC on Windows yet. + +```sh +scripts/build-windows.sh # ~2 min on Strix Halo once the downloads are cached +``` + +The script writes two single, static executables: +- **`/1bit.exe`** (3.2 MB). It imports only `KERNEL32`, `WS2_32` and the UCRT that ships with Windows 10. +- **`/llama-server.exe`**, the Vulkan pin (`third_party/llama.cpp-vulkan`) built for Windows. It also imports `vulkan-1.dll`, from the GPU driver. + +Keep the two side by side: `1bit.exe serve -m model.gguf` finds `llama-server.exe` next to itself. `--llama-server PATH` or `ONEBIT_LLAMA_SERVER` names another one. + +## What it downloads + +Every download is pinned and checked against its sha256: + +| input | why | +| --- | --- | +| llvm-mingw 20260922 (clang 23, UCRT) | the engine is C++26; distribution MinGW GCC 13 is too old | +| PCRE2 10.48 | the tokenizer, linked statically | +| Vulkan-Headers `vulkan-sdk-1.4.357.0` | llama.cpp's Vulkan backend | +| the Vulkan loader's `vulkan-1.def`, same tag | the import library, made with `llvm-dlltool`: no Vulkan SDK or Windows machine needed | +| SPIRV-Headers, same tag | llama.cpp's Vulkan backend | + +Host tools: `cmake`, `git`, `curl`, a native C/C++ compiler (llama.cpp's shader generator runs on the build machine) and `glslc`. + +## What changed for Windows + +- **Backends.** `1bit serve` starts its backends with `CreateProcess`, inside a job object that kills them when its last handle closes. So a backend dies with `1bit.exe` for any reason, as `PR_SET_PDEATHSIG` does on Linux; the Linux and macOS paths are unchanged. Windows has no SIGTERM, so a backend stops with `TerminateProcess`. +- **Model files.** Model and safetensors files are mapped through `npu/file_map.{h,cpp}` (mmap on POSIX, `CreateFileMapping` on Windows). +- **cpp-httplib.** It is built at the Windows 10 API level, with its non-blocking DNS lookup off, because MinGW's headers lack `GetAddrInfoExCancel` and the engine only ever connects to 127.0.0.1. +- **`1bit comfy`.** It spawns `comfyui_cpp.exe` and waits for it, since there is no exec in place on Windows. +- **llama.cpp workaround.** The pinned llama.cpp uses `std::function` in `ggml-vulkan-types.h` without including ``. libc++ does not include it transitively, so the script adds it from the command line. + +## Tested + +On Strix Halo under Wine 11, whose Vulkan goes to the host's RADV driver, so on the real Radeon 8060S. Measured 2026-09-25: +- `1bit.exe serve -m Qwen3-0.6B-Q4_K_M.gguf` found `llama-server.exe` beside it, answered "The capital of France is Paris." (132 tok/s) and "Three colors: green, red, and blue.", and streamed a reply as SSE. +- `taskkill /F /IM 1bit.exe` left no `llama-server.exe` behind. +- The Linux build of the same sources passes its tests. + +It has not run on a real Windows machine yet. + +## Next + +- **An ONNX Runtime GenAI backend:** Lemonade's ONNX and hybrid models, and the Ryzen AI NPU on Windows through the Vitis AI execution provider. +- **The NPU lane on Windows XRT.** diff --git a/laya/safetensors.cpp b/laya/safetensors.cpp index e93672c8..1f352db5 100644 --- a/laya/safetensors.cpp +++ b/laya/safetensors.cpp @@ -19,10 +19,7 @@ #include #include -#include -#include -#include -#include +#include "file_map.h" namespace onebit::laya { @@ -50,21 +47,9 @@ inline float bf16_to_f32(uint16_t bf) { bool Safetensors::open(const std::string& path) { err_.clear(); - const int fd = ::open(path.c_str(), O_RDONLY); - if (fd < 0) { - err_ = "cannot open " + path; - return false; - } - struct stat st {}; - ::fstat(fd, &st); - size_ = size_t(st.st_size); - void* m = ::mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd, 0); - ::close(fd); - if (m == MAP_FAILED) { - err_ = "cannot map " + path; - return false; - } - map_ = static_cast(m); + const uint8_t* m = onebit::npu::map_file(path, size_, &err_); + if (!m) return false; + map_ = const_cast(m); uint64_t header = 0; if (size_ < 8) { @@ -96,7 +81,7 @@ bool Safetensors::open(const std::string& path) { } Safetensors::~Safetensors() { - if (map_) ::munmap(map_, size_); + onebit::npu::unmap_file(map_, size_); } const Tensor* Safetensors::find(const std::string& name) const { diff --git a/npu/file_map.cpp b/npu/file_map.cpp new file mode 100644 index 00000000..e67b919d --- /dev/null +++ b/npu/file_map.cpp @@ -0,0 +1,80 @@ +// Copyright 2026 bong-water-water-bong +// SPDX-License-Identifier: Apache-2.0 +// +// 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. +#include "file_map.h" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#else +#include +#include +#include +#include +#endif + +namespace onebit::npu { + +const uint8_t* map_file(const std::string& path, size_t& size, std::string* why) { + size = 0; +#ifdef _WIN32 + const HANDLE f = ::CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (f == INVALID_HANDLE_VALUE) { + if (why) *why = "cannot open " + path; + return nullptr; + } + LARGE_INTEGER sz{}; + ::GetFileSizeEx(f, &sz); + const HANDLE m = sz.QuadPart ? ::CreateFileMappingA(f, nullptr, PAGE_READONLY, 0, 0, nullptr) : nullptr; + void* p = m ? ::MapViewOfFile(m, FILE_MAP_READ, 0, 0, 0) : nullptr; + if (m) ::CloseHandle(m); + ::CloseHandle(f); + if (!p) { + if (why) *why = "cannot map " + path; + return nullptr; + } + size = size_t(sz.QuadPart); + return static_cast(p); +#else + const int fd = ::open(path.c_str(), O_RDONLY); + if (fd < 0) { + if (why) *why = "cannot open " + path; + return nullptr; + } + struct stat st{}; + ::fstat(fd, &st); + void* p = st.st_size ? ::mmap(nullptr, size_t(st.st_size), PROT_READ, MAP_PRIVATE, fd, 0) : MAP_FAILED; + ::close(fd); + if (p == MAP_FAILED) { + if (why) *why = "cannot map " + path; + return nullptr; + } + size = size_t(st.st_size); + return static_cast(p); +#endif +} + +void unmap_file(const uint8_t* data, size_t size) { + if (!data) return; +#ifdef _WIN32 + (void)size; + ::UnmapViewOfFile(data); +#else + ::munmap(const_cast(data), size); +#endif +} + +} // namespace onebit::npu diff --git a/npu/file_map.h b/npu/file_map.h new file mode 100644 index 00000000..25ac594c --- /dev/null +++ b/npu/file_map.h @@ -0,0 +1,30 @@ +// Copyright 2026 bong-water-water-bong +// SPDX-License-Identifier: Apache-2.0 +// +// 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. +// A whole file mapped read-only (the Q4NX model, Laya's safetensors): mmap on POSIX, a file +// mapping on Windows. +#pragma once + +#include +#include +#include + +namespace onebit::npu { + +// The file's bytes, or nullptr with *why ("cannot open " / "cannot map "). +// size is the file's size. The mapping outlives the file handle. +const uint8_t* map_file(const std::string& path, size_t& size, std::string* why); +void unmap_file(const uint8_t* data, size_t size); + +} // namespace onebit::npu diff --git a/npu/model.cpp b/npu/model.cpp index 20150427..3672a08c 100644 --- a/npu/model.cpp +++ b/npu/model.cpp @@ -17,13 +17,11 @@ #include +#include "file_map.h" + #include -#include #include #include -#include -#include -#include namespace onebit::npu { @@ -40,15 +38,10 @@ int Tensor::tiles() const { Model::Model(const std::string& dir) { const std::string path = dir + "/model.q4nx"; - const int fd = ::open(path.c_str(), O_RDONLY); - if (fd < 0) throw std::runtime_error("cannot open " + path); - struct stat st{}; - ::fstat(fd, &st); - size_ = size_t(st.st_size); - void* m = ::mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd, 0); - ::close(fd); - if (m == MAP_FAILED) throw std::runtime_error("cannot map " + path); - map_ = static_cast(m); + std::string why; + const uint8_t* m = map_file(path, size_, &why); + if (!m) throw std::runtime_error(why); + map_ = const_cast(m); uint64_t header = 0; if (size_ < 8) throw std::runtime_error(path + ": truncated"); @@ -89,7 +82,7 @@ Model::Model(const std::string& dir) { } Model::~Model() { - if (map_) ::munmap(map_, size_); + unmap_file(map_, size_); } const Tensor* Model::find(const std::string& name) const { diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100755 index 00000000..a5864eb2 --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# 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. +# +# build-windows.sh +# +# Cross-builds the engine for Windows 10+ x64 on Linux (docs/windows.md): /1bit.exe and, +# from the Vulkan pin (third_party/llama.cpp-vulkan), /llama-server.exe with the Vulkan +# backend. Both are single static executables; 1bit.exe finds llama-server.exe beside it. +# Everything it downloads is pinned and checked against its sha256: llvm-mingw (clang 23: the +# engine is C++26), PCRE2 (the tokenizer), Vulkan-Headers, the Vulkan loader's export list +# (vulkan-1.def -> the import library, so no Windows machine or Vulkan SDK is needed) and +# SPIRV-Headers. Host tools: cmake, git, curl, a native C/C++ compiler (for llama.cpp's +# shader generator) and glslc (shaderc). +set -euo pipefail +out=$(realpath -m "${1:?usage: build-windows.sh }") +root=$(cd "$(dirname "$0")/.." && pwd) +work=$out/work +mkdir -p "$work" "$out" +jobs=$(nproc) + +LLVM_MINGW=https://github.com/mstorsjo/llvm-mingw/releases/download/20260922/llvm-mingw-20260922-ucrt-ubuntu-22.04-x86_64.tar.xz +LLVM_MINGW_SHA=bb7bb7654b33d5aa8712acb837c963b2e0c56352560c76105270a3268c665c21 +PCRE2=https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.48/pcre2-10.48.tar.gz +PCRE2_SHA=ebcc25aadf2a51fa1fefa9b8bc9e7a79b3dae86870a0f1152a22e42befd46888 +VK_TAG=vulkan-sdk-1.4.357.0 +VK_HEADERS=https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/$VK_TAG.tar.gz +VK_HEADERS_SHA=e87dce08116151f6b6d7de6b6faf41498e87e6cf848ff16fa3bd5402190ad4a3 +VK_DEF=https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/$VK_TAG/loader/vulkan-1.def +VK_DEF_SHA=9ba339b7f5ee2df28487698a6840ecf095fac58b415c2c89ad9f161e9d316378 +SPIRV_HEADERS=https://github.com/KhronosGroup/SPIRV-Headers/archive/refs/tags/$VK_TAG.tar.gz +SPIRV_HEADERS_SHA=4d703067a7e06331ccb37bdfed3f9b7879cc61969a2689ae95c95db34a47ff07 + +command -v glslc >/dev/null || { echo "build-windows.sh: glslc (shaderc) is required"; exit 1; } + +# fetch : download once, verify every time +fetch() { + local f="$work/$3" + if ! { [ -f "$f" ] && echo "$2 $f" | sha256sum -c --quiet 2>/dev/null; }; then + curl -fsSL -o "$f.part" "$1" + echo "$2 $f.part" | sha256sum -c --quiet || { echo "build-windows.sh: $1: checksum mismatch"; exit 1; } + mv "$f.part" "$f" + fi + echo "$f" +} +unpack() { # unpack + [ -d "$2" ] || { mkdir -p "$2.tmp" && tar xf "$1" -C "$2.tmp" --strip-components=1 && mv "$2.tmp" "$2"; } +} + +# 1. the toolchain, and a CMake toolchain file for it +unpack "$(fetch $LLVM_MINGW $LLVM_MINGW_SHA llvm-mingw.tar.xz)" "$work/llvm-mingw" +T=$work/llvm-mingw +prefix=$work/prefix +mkdir -p "$prefix/include" "$prefix/lib" +cat > "$work/toolchain.cmake" < "$work/host.cmake" + +# 2. PCRE2, static +if [ ! -f "$prefix/lib/libpcre2-8.a" ]; then + unpack "$(fetch $PCRE2 $PCRE2_SHA pcre2.tar.gz)" "$work/pcre2" + cmake -S "$work/pcre2" -B "$work/pcre2/build" -DCMAKE_TOOLCHAIN_FILE="$work/toolchain.cmake" \ + -DCMAKE_INSTALL_PREFIX="$prefix" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DPCRE2_BUILD_TESTS=OFF -DPCRE2_BUILD_PCRE2GREP=OFF > /dev/null + cmake --build "$work/pcre2/build" -j"$jobs" > /dev/null + cmake --install "$work/pcre2/build" > /dev/null +fi + +# 3. Vulkan: the headers, the loader's import library from its export list, SPIRV-Headers (header +# only: installed into the Windows prefix, where the cross compiler looks) +unpack "$(fetch $VK_HEADERS $VK_HEADERS_SHA vulkan-headers.tar.gz)" "$work/vulkan-headers" +cp -r "$work/vulkan-headers/include/." "$prefix/include/" +"$T/bin/llvm-dlltool" -d "$(fetch $VK_DEF $VK_DEF_SHA vulkan-1.def)" -l "$prefix/lib/libvulkan-1.a" -m i386:x86-64 +unpack "$(fetch $SPIRV_HEADERS $SPIRV_HEADERS_SHA spirv-headers.tar.gz)" "$work/spirv-headers" +cmake -S "$work/spirv-headers" -B "$work/spirv-headers/build" -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DSPIRV_HEADERS_ENABLE_TESTS=OFF > /dev/null +cmake --install "$work/spirv-headers/build" > /dev/null + +# 4. 1bit.exe: serve, route, comfy (no NPU lane or HRX on Windows yet) +PKG_CONFIG_LIBDIR=$prefix/lib/pkgconfig cmake -S "$root" -B "$work/engine" -DCMAKE_TOOLCHAIN_FILE="$work/toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release -DONEBIT_NPU=OFF -DONEBIT_HRX=OFF > "$work/engine.cmake.log" +cmake --build "$work/engine" --target onebit -j"$jobs" > "$work/engine.build.log" 2>&1 || { tail -20 "$work/engine.build.log"; exit 1; } +cp "$work/engine/1bit.exe" "$out/" + +# 5. llama-server.exe with Vulkan, from the Vulkan pin. The pinned llama.cpp uses std::function +# without in ggml-vulkan-types.h, which libc++ does not include transitively: it is +# included from the command line until upstream adds it. +git -C "$root" submodule update --init third_party/llama.cpp-vulkan +cmake -S "$root/third_party/llama.cpp-vulkan" -B "$work/llama" -DCMAKE_TOOLCHAIN_FILE="$work/toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DGGML_NATIVE=OFF -DGGML_OPENMP=OFF -DGGML_VULKAN=ON \ + -DGGML_VULKAN_SHADERS_GEN_TOOLCHAIN="$work/host.cmake" -DSPIRV-Headers_DIR="$prefix/share/cmake/SPIRV-Headers" \ + -DVulkan_INCLUDE_DIR="$prefix/include" -DVulkan_LIBRARY="$prefix/lib/libvulkan-1.a" \ + -DVulkan_GLSLC_EXECUTABLE="$(command -v glslc)" -DLLAMA_CURL=OFF -DLLAMA_BUILD_TESTS=OFF \ + -DLLAMA_BUILD_EXAMPLES=OFF -DCMAKE_EXE_LINKER_FLAGS=-static \ + "-DCMAKE_C_FLAGS=-D_WIN32_WINNT=0x0A00" "-DCMAKE_CXX_FLAGS=-D_WIN32_WINNT=0x0A00 -include functional" \ + > "$work/llama.cmake.log" +cmake --build "$work/llama" --target llama-server -j"$jobs" > "$work/llama.build.log" 2>&1 || { tail -20 "$work/llama.build.log"; exit 1; } +cp "$work/llama/bin/llama-server.exe" "$out/" + +ls -la "$out/1bit.exe" "$out/llama-server.exe" diff --git a/tools/site.py b/tools/site.py index 433c1578..fe4bdbef 100644 --- a/tools/site.py +++ b/tools/site.py @@ -77,7 +77,7 @@ ("Start", [("serve", "1bit serve"), ("lemonade", "Lemonade")]), ("Devices", [("npu", "NPU"), ("hrx", "HRX + Vulkan"), ("vulkan", "Vulkan (upstream)"), ("lean", "Lean (ROCmFP4, ROCmI4)"), ("zinc", "ZINC"), ("apple", "Apple Silicon")]), - ("Components", [("moe-streaming", "MoE streaming"), ("laya", "Laya router"), ("registry", "Model registry"), ("arch-gaps", "Architecture gaps"), ("comfyui", "ComfyUI.cpp"), ("tokenizers", "Tokenizers"), ("kernel", "Linux kernel")]), + ("Components", [("moe-streaming", "MoE streaming"), ("laya", "Laya router"), ("registry", "Model registry"), ("arch-gaps", "Architecture gaps"), ("windows", "Windows"), ("comfyui", "ComfyUI.cpp"), ("tokenizers", "Tokenizers"), ("kernel", "Linux kernel")]), ("Project", [("PORTING", "Porting map")]), ]