From 39e385782c0f3bbd6ef41373ead7f2ebf4f7e2ef Mon Sep 17 00:00:00 2001 From: David Coles Date: Wed, 21 Feb 2024 22:46:24 -0800 Subject: [PATCH 1/2] Allow building on Windows using `clang-cl` toolchain It's not possible to build `gemma.cpp` with the standard MSVC front-end as it doesn't support arrays more than `0x7ffffffff` bytes (see Compiler Error C2148), however this isn't a problem with the optional Visual Studio Clang/LLVM frontend. This can be specified using the `-T` flag when running CMake: ``` $ cmake -B build -T ClangCL $ cmake --build build --config Release ``` Windows doesn't provide `pread`/`pwrite` so this must be emulated using the `ReadFile`/`WriteFile` Win32 APIs. `_CRT_SECURE_NO_WARNINGS` is defined to prevent a large number of warnings about using "depricated" function names (e.g. `close` instead of `_close`). `NOMINMAX` is defined to prevent the `min`/`max` macros from `windows.h` from conflicting with expressions like `std::min`. Generally libraries should avoid including `windows.h` in their public headers or define `WIN32_LEAN_AND_MEAN` before including the `windows.h` header, but this unfortunately isn't always the case. --- CMakeLists.txt | 4 ++ compression/blob_store.cc | 97 +++++++++++++++++++++++++++++++++++---- util/app.h | 2 + 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3858968..e5ab191 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,8 @@ target_link_libraries(gemma hwy hwy_contrib sentencepiece) target_include_directories(gemma PRIVATE ./) FetchContent_GetProperties(sentencepiece) target_include_directories(gemma PRIVATE ${sentencepiece_SOURCE_DIR}) +target_compile_definitions(gemma PRIVATE $<$:_CRT_SECURE_NO_WARNINGS NOMINMAX>) +target_compile_options(gemma PRIVATE $<$:-Wno-deprecated-declarations>) ## Library Target @@ -77,3 +79,5 @@ set_target_properties(libgemma PROPERTIES PREFIX "") target_include_directories(libgemma PUBLIC ./) target_link_libraries(libgemma hwy hwy_contrib sentencepiece) target_include_directories(libgemma PRIVATE ${sentencepiece_SOURCE_DIR}) +target_compile_definitions(libgemma PRIVATE $<$:_CRT_SECURE_NO_WARNINGS NOMINMAX>) +target_compile_options(libgemma PRIVATE $<$:-Wno-deprecated-declarations>) diff --git a/compression/blob_store.cc b/compression/blob_store.cc index 8d6c1d0..550c727 100644 --- a/compression/blob_store.cc +++ b/compression/blob_store.cc @@ -16,11 +16,16 @@ // copybara:import_next_line:gemma_cpp #include "compression/blob_store.h" -#include // open #include #include // SEEK_END - unistd isn't enough for IDE. #include // O_RDONLY -#include // read, close +#include // open +#if HWY_OS_WIN +#include // read, write, close +#include +#else +#include // read, write, close +#endif #include #include @@ -30,6 +35,54 @@ #include "hwy/contrib/thread_pool/thread_pool.h" #include "hwy/detect_compiler_arch.h" +namespace { +#if HWY_OS_WIN + +// pread is not supported on Windows +static int64_t pread(int fd, void* buf, uint64_t size, uint64_t offset) { + HANDLE file = reinterpret_cast(_get_osfhandle(fd)); + if (file == INVALID_HANDLE_VALUE) { + return -1; + } + + OVERLAPPED overlapped = {0}; + overlapped.Offset = offset & 0xFFFFFFFF; + overlapped.OffsetHigh = (offset >> 32) & 0xFFFFFFFF; + + DWORD bytes_read; + if (!ReadFile(file, buf, size, &bytes_read, &overlapped)) { + if (GetLastError() != ERROR_HANDLE_EOF) { + return -1; + } + } + + return bytes_read; +} + +// pwrite is not supported on Windows +static int64_t pwrite(int fd, const void* buf, uint64_t size, uint64_t offset) { + HANDLE file = reinterpret_cast(_get_osfhandle(fd)); + if (file == INVALID_HANDLE_VALUE) { + return -1; + } + + OVERLAPPED overlapped = {0}; + overlapped.Offset = offset & 0xFFFFFFFF; + overlapped.OffsetHigh = (offset >> 32) & 0xFFFFFFFF; + + DWORD bytes_written; + if (!WriteFile(file, buf, size, &bytes_written, &overlapped)) { + if (GetLastError() != ERROR_HANDLE_EOF) { + return -1; + } + } + + return bytes_written; +} + +#endif +} + namespace gcpp { hwy::uint128_t MakeKey(const char* string) { @@ -64,19 +117,30 @@ static void EnqueueChunkRequests(uint64_t offset, uint64_t size, uint8_t* data, } } + struct IO { // Returns size in bytes or 0. static uint64_t FileSize(const char* filename) { int fd = open(filename, O_RDONLY); - if (fd >= 0) { - const off_t size = lseek(fd, 0, SEEK_END); - HWY_ASSERT(close(fd) != -1); - if (size != static_cast(-1)) { - return static_cast(size); - } + if (fd < 0) { + return 0; } - return 0; +#if HWY_OS_WIN + const int64_t size = _lseeki64(fd, 0, SEEK_END); + HWY_ASSERT(close(fd) != -1); + if (size < 0) { + return 0; + } +#else + const off_t size = lseek(fd, 0, SEEK_END); + HWY_ASSERT(close(fd) != -1); + if (size == static_cast(-1)) { + return 0; + } +#endif + + return static_cast(size); } static bool Read(int fd, uint64_t offset, uint64_t size, void* to) { @@ -252,7 +316,14 @@ class BlobStore { #pragma pack(pop) BlobError BlobReader::Open(const char* filename) { +#if HWY_OS_WIN + DWORD flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN; + HANDLE file = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, flags, nullptr); + if (file == INVALID_HANDLE_VALUE) return __LINE__; + fd_ = _open_osfhandle(reinterpret_cast(file), _O_RDONLY); +#else fd_ = open(filename, O_RDONLY); +#endif if (fd_ < 0) return __LINE__; #if _POSIX_C_SOURCE >= 200112L @@ -330,7 +401,14 @@ BlobError BlobWriter::WriteAll(hwy::ThreadPool& pool, keys_.data(), blobs_.data(), keys_.size()); // Create/replace existing file. +#if HWY_OS_WIN + DWORD flags = FILE_ATTRIBUTE_NORMAL; + HANDLE file = CreateFileA(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, flags, nullptr); + if (file == INVALID_HANDLE_VALUE) return __LINE__; + const int fd = _open_osfhandle(reinterpret_cast(file), _O_WRONLY); +#else const int fd = open(filename, O_CREAT | O_RDWR | O_TRUNC, 0644); +#endif if (fd < 0) return __LINE__; std::atomic_flag err = ATOMIC_FLAG_INIT; @@ -341,6 +419,7 @@ BlobError BlobWriter::WriteAll(hwy::ThreadPool& pool, err.test_and_set(); } }); + HWY_ASSERT(close(fd) != -1); if (err.test_and_set()) return __LINE__; return 0; } diff --git a/util/app.h b/util/app.h index 966fa41..bd665a4 100644 --- a/util/app.h +++ b/util/app.h @@ -18,7 +18,9 @@ #ifndef THIRD_PARTY_GEMMA_CPP_UTIL_APP_H_ #define THIRD_PARTY_GEMMA_CPP_UTIL_APP_H_ +#if HWY_OS_LINUX #include +#endif #include #include // std::clamp From ceea495693949eba86cab2caf6cd28169fb7de61 Mon Sep 17 00:00:00 2001 From: David Coles Date: Fri, 23 Feb 2024 00:12:21 -0800 Subject: [PATCH 2/2] Add CMakePresets and update README instructions Using a `CMakePresets.json` file makes it much easier to manage several alternate build configurations, such as the "ClangCL" build for Windows. This also makes it easier for tools like VSCode to run CMake-based builds. --- CMakePresets.json | 59 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 41 ++++++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 CMakePresets.json diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..5fe13c8 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,59 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 11, + "patch": 0 + }, + "configurePresets": [ + { + "name": "__defaults__", + "hidden": true, + "binaryDir": "${sourceDir}/build" + }, + { + "name": "make", + "inherits": "__defaults__", + "displayName": "Make", + "description": "Unix Makefiles", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build" + }, + { + "name": "windows", + "inherits": "__defaults__", + "displayName": "Windows", + "description": "Visual Studio 2022 with Clang/LLVM frontend", + "generator": "Visual Studio 17 2022", + "toolset": "ClangCL", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + } + ], + "buildPresets": [ + { + "name": "__defaults__", + "hidden": true, + "targets": [ + "gemma", + "libgemma" + ] + }, + { + "name": "make", + "inherits": "__defaults__", + "displayName": "Unix Makefiles", + "configurePreset": "make" + }, + { + "name": "windows", + "inherits": "__defaults__", + "displayName": "Windows", + "configuration": "Release", + "configurePreset": "windows" + } + ] + } diff --git a/README.md b/README.md index ab7bf2d..ee53551 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,16 @@ Before starting, you should have installed: least C++17. - `tar` for extracting archives from Kaggle. +Building natively on Windows requires the Visual Studio 2012 Build Tools with the +optional Clang/LLVM C++ frontend (`clang-cl`). This can be installed from the +command line with +[`winget`](https://learn.microsoft.com/en-us/windows/package-manager/winget/): + +```sh +winget install --id Kitware.CMake +winget install --id Microsoft.VisualStudio.2022.BuildTools --force --override "--passive --wait --add Microsoft.VisualStudio.Workload.VCTools;installRecommended --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset" +``` + ### Step 1: Obtain model weights and tokenizer from Kaggle Visit [the Gemma model page on @@ -104,25 +114,36 @@ The build system uses [CMake](https://cmake.org/). To build the gemma inference runtime, create a build directory and generate the build files using `cmake` from the top-level project directory: -```sh -cmake -B build -``` - -Then run `make` to build the `./gemma` executable: +#### Unix-like Platforms ```sh -cd build -make -j [number of parallel threads to use] gemma +# Configure `build` directory +cmake --preset make + +# Build project using make +cmake --build --preset make -j [number of parallel threads to use] ``` -For example, `make -j4 gemma` will build using 4 threads. If this is successful, -you should now have a `gemma` executable in the `build/` directory. If the -`nproc` command is available, you can use `make -j$(nproc) gemma`. +If the `nproc` command is available, you can use `-j $(nproc)`. + +If this is successful, you should now have a `gemma` executable in the `build/` directory. > [!NOTE] > On Windows Subsystem for Linux (WSL) users should set the number of > parallel threads to 1. Using a larger number may result in errors. +#### Windows + +```sh +# Configure `build` directory +cmake --preset windows + +# Build project using Visual Studio Build Tools +cmake --build --preset windows -j [number of parallel threads to use] +``` + +If this is successful, you should now have a `gemma.exe` executable in the `build/` directory. + ### Step 4: Run You can now run `gemma` from inside the `build/` directory.