Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Lukylix/3cda5ba2985c92e0ddd978f478d1ee03 to your computer and use it in GitHub Desktop.

Select an option

Save Lukylix/3cda5ba2985c92e0ddd978f478d1ee03 to your computer and use it in GitHub Desktop.
PowerShell script to build llama.cpp TurboQuant fork with AMD ROCm (Windows, RX 7000 support). Includes GPU auto-detection, ROCm auto-download, patch support, and full build pipeline.
From 981a73b05044e2077dca30b188be1db6fa6487e6 Mon Sep 17 00:00:00 2001
From: lukylix <[email protected]>
Date: Sat, 4 Apr 2026 08:20:44 +0200
Subject: [PATCH] fix: update ggml rpc protocol for GGML_OP_COUNT change
---
ggml/include/ggml-rpc.h | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ggml/include/ggml-rpc.h b/ggml/include/ggml-rpc.h
index 1c11495..d9ab2d6 100644
--- a/ggml/include/ggml-rpc.h
+++ b/ggml/include/ggml-rpc.h
@@ -8,10 +8,10 @@ extern "C" {
#define RPC_PROTO_MAJOR_VERSION 3
#define RPC_PROTO_MINOR_VERSION 6
-#define RPC_PROTO_PATCH_VERSION 1
+#define RPC_PROTO_PATCH_VERSION 2
#ifdef __cplusplus
-static_assert(GGML_OP_COUNT == 96, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION");
+static_assert(GGML_OP_COUNT == 97, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION");
#endif
#define GGML_RPC_MAX_SERVERS 16
--
2.50.1.windows.1

TurboQuant llama.cpp + ROCm Build Script

This PowerShell script automates building TurboQuant's optimized version of llama.cpp with ROCm support for AMD GPUs.

Features

  • Automatically detects your AMD GPU and selects the appropriate ROCm architecture
  • Downloads and installs the latest ROCm version (7.13.0a20260404)
  • Builds with TurboQuant optimizations for extreme KV cache quantization
  • Includes all necessary ROCm runtime libraries in the build directory
  • No separate ROCm installation required

Requirements

  • Windows 10/11
  • PowerShell 5.1 or later
  • Internet connection (for downloading ROCm and dependencies)
  • AMD GPU with ROCm support (tested on RX 7900 XT)

Usage

  1. Download this script and place it in your desired directory
  2. Create a patches subdirectory and download the patch files into it:
  3. Run the script in an elevated PowerShell session:
    .\build-llama-turboquant.ps1

Optional Parameters

  • -SkipPrerequisites:$true: Skip ROCm installation if already present
  • -ReuseROCm:$false: Force re-extraction of ROCm (default: reuse existing)
  • -KeepROCmTarball:$false: Delete the downloaded ROCm tarball after extraction

Output

The script will:

  1. Clone TurboQuant's llama.cpp fork
  2. Apply any patches from the patches directory
  3. Configure and build with CMake/Ninja
  4. Copy all necessary ROCm runtime libraries
  5. Create a summary file (build-summary.txt)

Build Location

After successful completion, you'll find:

  • Binaries: llama.cpp\build\bin
  • All ROCm DLLs included in the same directory
  • No need for separate ROCm installation

Testing

You can test your build with:

cd llama.cpp\build\bin
.\llama-cli.exe -m <your-model.gguf> -p "Hello world" -n 50

Notes

  • The script automatically detects your GPU architecture
  • ROCm installation is cached for future builds (can be disabled)
  • All dependencies are downloaded automatically
  • Build type is Release by default

License

This script is provided as-is. The TurboQuant llama.cpp fork has its own license.

<#
.SYNOPSIS
Build llama.cpp TurboQuant fork with AMD ROCm support for Windows
.DESCRIPTION
This script automates the build process for the TurboQuant fork of llama.cpp
with AMD ROCm GPU acceleration support. It follows the build steps from the
llamacpp-rocm GitHub Actions workflow and adapts them for TurboQuant.
Tested With GPU: AMD Radeon RX 7900 XT (gfx1100 architecture)
.PARAMETER ROCmVersion
ROCm version to use. Default: "latest" (auto-detect latest nightly)
.PARAMETER GfxTarget
AMD GPU architecture target. Default: "gfx110X" (for RX 7900 series)
Options: gfx110X, gfx1151, gfx1150, gfx120X, gfx103X
.PARAMETER BuildDir
Directory for build artifacts. Default: ".\build"
.PARAMETER CleanBuild
Force clean build by removing existing directories and repositories. When $true, performs a fresh clone; when $false, reuses existing repository if available. Default: $false
.PARAMETER SkipPrerequisites
Skip installation of prerequisites (Visual Studio, CMake, Ninja). Default: $false
.PARAMETER DetectGpu
Auto-detect GPU and set appropriate GFX target. Default: $true
.PARAMETER KeepROCmTarball
Keep the downloaded ROCm tarball after extraction. Default: $true
.PARAMETER DetectGpuOnly
Only detect GPU information without building. Useful for testing detection. Default: $false
.EXAMPLE
.\build-llama-turboquant.ps1
.EXAMPLE
.\build-llama-turboquant.ps1 -ROCmVersion "7.13.0a20260318" -GfxTarget "gfx110X"
.EXAMPLE
.\build-llama-turboquant.ps1 -SkipPrerequisites:$true -CleanBuild:$true
.NOTES
Author: Lukylix/AI Assistant
Date: 2026-04-03
Requires: Windows 10/11, PowerShell 5.1+, Administrator privileges (for prerequisites)
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$ROCmVersion = "latest",
[Parameter()]
[ValidateSet("gfx110X", "gfx1151", "gfx1150", "gfx120X", "gfx103X")]
[string]$GfxTarget = "gfx110X",
[Parameter()]
[string]$BuildDir = ".\build",
[Parameter()]
[bool]$CleanBuild = $false,
[Parameter()]
[bool]$SkipPrerequisites = $false,
[Parameter()]
[bool]$DetectGpu = $true,
[Parameter()]
[bool]$KeepROCmTarball = $true,
[Parameter()]
[bool]$DetectGpuOnly = $false
)
$ProgressPreference = "SilentlyContinue" # Faster downloads
# Global cache for GPU detection result
$Global:GpuInfoCache = $null
# Centralized configuration object
$Config = @{
RepoUrl = "https://github.com/TheTom/llama-cpp-turboquant.git"
RepoBranch = "feature/turboquant-kv-cache"
RocmInstallPath = "C:\\opt\\rocm"
RocmS3BaseUrl = "https://therock-nightly-tarball.s3.amazonaws.com"
CMakeGenerator = "Ninja"
CMakeBuildType = "Release"
FallbackGfxTarget = "gfx1030"
BuildDirName = "build"
}
# ROCm configuration
$GPU_EXAMPLE = "AMD Radeon RX 7900 XT"
# Context management
function Initialize-BuildContext {
param(
[string]$GfxTarget,
[string]$ROCmVersion
)
return @{
GfxTarget = $GfxTarget
ROCmVersion = $ROCmVersion
RepoPath = "llama.cpp"
CommitHash = $null
S3Target = $null
MappedTargets= $null
RocmInfo = $null
}
}
function Set-S3Target {
param($Context)
# Use the new resolution function to find a valid S3 target
$Context.S3Target = Resolve-S3Target -GpuTarget $Context.GfxTarget -BaseUrl $Config.RocmS3BaseUrl
return $Context
}
function Invoke-DownloadROCm {
param($Context)
$Context.RocmInfo = Download-ROCm `
-Version $Context.ROCmVersion `
-S3Target $Context.S3Target `
-ReuseExisting $KeepROCmTarball
return $Context
}
function Set-MappedTargets {
param($Context)
$Context.MappedTargets = Get-MappedGfxTargets -GfxTarget $Context.GfxTarget
return $Context
}
# Logging configuration
$VerboseLogging = $true
# Unified logging helper
function Write-Log {
param(
[Parameter(Mandatory)]
[string]$Message,
[ValidateSet("INFO","SUCCESS","WARN","ERROR","STEP","DEBUG")]
[string]$Level = "INFO",
[switch]$NoTimestamp
)
$timestamp = Get-Date -Format "HH:mm:ss"
$prefix = switch ($Level) {
"STEP" { "[STEP]" }
"SUCCESS" { "[OK] " }
"WARN" { "[WARN]" }
"ERROR" { "[FAIL]" }
"DEBUG" { "[DBG] " }
default { "[INFO]" }
}
$color = switch ($Level) {
"SUCCESS" { "Green" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
"STEP" { "Cyan" }
"DEBUG" { "DarkGray" }
default { "Gray" }
}
if ($NoTimestamp) {
Write-Host "$prefix $Message" -ForegroundColor $color
} else {
Write-Host "[$timestamp] $prefix $Message" -ForegroundColor $color
}
}
# Legacy wrapper for backward compatibility (remove after updating all calls)
function Write-ColorOutput {
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
$Message,
[Parameter()]
[ValidateSet("Info", "Success", "Warning", "Error")]
[string]$Level = "Info"
)
$logLevel = switch ($Level) {
"Info" { "INFO" }
"Success" { "SUCCESS" }
"Warning" { "WARN" }
"Error" { "ERROR" }
default { "INFO" }
}
Write-Log -Message $Message -Level $logLevel
}
function Write-Step {
param(
[string]$Title,
[string[]]$Details = @()
)
Write-Log $Title "STEP"
foreach ($line in $Details) {
Write-Host " $line" -ForegroundColor Gray
}
}
# ============================================================================
# Helper Functions
# ============================================================================
function Test-CommandExists {
param([string]$Command)
$oldPreference = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
try {
if (Get-Command $Command -ErrorAction SilentlyContinue) {
return $true
}
return $false
}
finally {
$ErrorActionPreference = $oldPreference
}
}
function Test-IsAdmin {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Install-Prerequisites {
if ($VerboseLogging) {
Write-Log "Checking and installing prerequisites..." "DEBUG"
}
# Check if running as administrator for installations
if (-not (Test-IsAdmin)) {
Write-Log "Administrator privileges required" "WARN"
Write-Log "Please run as Administrator or use -SkipPrerequisites" "WARN"
exit 1
}
# Check for Visual Studio Build Tools
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vsWhere) {
$vsPath = & $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if ($vsPath) {
Write-Log "Visual Studio Build Tools found: $vsPath" "SUCCESS"
} else {
if ($VerboseLogging) { Write-Log "Installing Visual Studio Build Tools..." "DEBUG" }
Install-VisualStudioBuildTools
}
} else {
if ($VerboseLogging) { Write-Log "Installing Visual Studio Build Tools..." "DEBUG" }
Install-VisualStudioBuildTools
}
# Check for CMake
if (-not (Test-CommandExists "cmake")) {
if ($VerboseLogging) { Write-Log "Installing CMake via winget..." "DEBUG" }
winget install --id Kitware.CMake --silent --accept-source-agreements --accept-package-agreements
# Refresh PATH
Refresh-Path
} else {
$cmakeVersion = (cmake --version | Select-Object -First 1)
Write-Log "CMake found: $cmakeVersion" "SUCCESS"
}
# Check for Ninja
if (-not (Test-CommandExists "ninja")) {
if ($VerboseLogging) { Write-Log "Installing Ninja build system..." "DEBUG" }
Install-Ninja
} else {
$ninjaVersion = (ninja --version)
Write-Log "Ninja found: version $ninjaVersion" "SUCCESS"
}
# Check for Perl (required for ROCm)
if (-not (Test-CommandExists "perl")) {
if ($VerboseLogging) { Write-Log "Installing Strawberry Perl..." "DEBUG" }
Install-StrawberryPerl
} else {
Write-Log "Perl found" "SUCCESS"
}
# Check for Git
if (-not (Test-CommandExists "git")) {
if ($VerboseLogging) { Write-Log "Installing Git..." "DEBUG" }
winget install --id Git.Git --silent --accept-source-agreements --accept-package-agreements
# Refresh PATH
Refresh-Path
} else {
Write-Log "Git found" "SUCCESS"
}
Write-Log "All prerequisites installed" "SUCCESS"
}
function Install-VisualStudioBuildTools {
$vsInstallerUrl = "https://aka.ms/vs/17/release/vs_buildtools.exe"
$vsInstallerPath = "$env:TEMP\vs_buildtools.exe"
if ($VerboseLogging) { Write-Log "Downloading Visual Studio Build Tools..." "DEBUG" }
Invoke-WebRequest -Uri $vsInstallerUrl -OutFile $vsInstallerPath -UseBasicParsing
if ($VerboseLogging) { Write-Log "Installing Visual Studio Build Tools (this may take several minutes)..." "DEBUG" }
$process = Start-Process -FilePath $vsInstallerPath -ArgumentList @(
"--quiet",
"--wait",
"--norestart",
"--add", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"--add", "Microsoft.VisualStudio.Component.VC.CMake.Project",
"--add", "Microsoft.VisualStudio.Component.VC.ATL",
"--add", "Microsoft.VisualStudio.Component.Windows11SDK.22621"
) -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Log "Visual Studio Build Tools installation failed with exit code $($process.ExitCode)" "ERROR"
exit 1
}
Remove-Item $vsInstallerPath -Force -ErrorAction SilentlyContinue
Write-Log "Visual Studio Build Tools installed successfully" "SUCCESS"
}
function Install-Ninja {
$ninjaUrl = "https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-win.zip"
$ninjaPath = "$env:TEMP\ninja-win.zip"
$ninjaDir = "C:\ninja"
New-Item -ItemType Directory -Force -Path $ninjaDir | Out-Null
Write-ColorOutput "Downloading Ninja..." "Info"
Invoke-WebRequest -Uri $ninjaUrl -OutFile $ninjaPath -UseBasicParsing
Write-ColorOutput "Extracting Ninja..." "Info"
Expand-Archive -Path $ninjaPath -DestinationPath $ninjaDir -Force
# Add to system PATH if not already present
$systemPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
if ($systemPath -notlike "*$ninjaDir*") {
[Environment]::SetEnvironmentVariable("Path", "$systemPath;$ninjaDir", "Machine")
}
$env:Path = "$env:Path;$ninjaDir"
Remove-Item $ninjaPath -Force -ErrorAction SilentlyContinue
Write-ColorOutput "Ninja installed successfully to $ninjaDir" "Success"
}
function Install-StrawberryPerl {
$perlUrl = "https://strawberryperl.com/download/5.32.1.1/strawberry-perl-5.32.1.1-64bit.msi"
$perlPath = "$env:TEMP\strawberry-perl.msi"
if ($VerboseLogging) { Write-Log "Downloading Strawberry Perl..." "DEBUG" }
Invoke-WebRequest -Uri $perlUrl -OutFile $perlPath -UseBasicParsing
if ($VerboseLogging) { Write-Log "Installing Strawberry Perl..." "DEBUG" }
$process = Start-Process msiexec.exe -ArgumentList "/i `"$perlPath`" /quiet /norestart" -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-ColorOutput "Strawberry Perl installation failed with exit code $($process.ExitCode)" "Error"
exit 1
}
# Add to PATH
$env:Path = "C:\Strawberry\perl\bin;$env:Path"
Remove-Item $perlPath -Force -ErrorAction SilentlyContinue
Write-Log "Strawberry Perl installed successfully" "SUCCESS"
}
function Get-ROCmS3Target {
param([string]$GfxTarget)
# Map GFX targets to S3 naming convention
$s3Target = $GfxTarget
switch ($GfxTarget) {
"gfx103X" { $s3Target = "${GfxTarget}-dgpu" }
"gfx110X" { $s3Target = "${GfxTarget}-all" }
"gfx120X" { $s3Target = "${GfxTarget}-all" }
}
return $s3Target
}
function Get-GfxTargetFromArchitecture {
param([string]$Arch)
if (-not $Arch) {
return $null
}
# Match gfx + digits (e.g. gfx1100, gfx1030)
if ($Arch -match 'gfx(\d{3})\d*') {
$family = $Matches[1] # e.g. 110, 103
return "gfx${family}X-all"
}
return $null
}
function Get-MappedGfxTargets {
param([string]$GfxTarget)
# Map shorthand targets to full architecture list
$mapped = switch ($GfxTarget) {
"gfx110X" { "gfx1100;gfx1101;gfx1102;gfx1103" }
"gfx103X" { "gfx1030;gfx1031;gfx1032;gfx1034" }
"gfx120X" { "gfx1200;gfx1201" }
"gfx1151" { "gfx1151" }
"gfx1150" { "gfx1150" }
default { $GfxTarget }
}
return $mapped
}
# S3 Target Candidate Resolution Functions
function Get-S3TargetCandidates {
param([string]$GpuTarget)
if (-not $GpuTarget) {
return @("gfx110X-all") # final fallback
}
# If user already provided full target
if ($GpuTarget -match '^gfx\d{3}X-(all|dgpu)$') {
return @($GpuTarget)
}
# If only family provided (gfx101X)
if ($GpuTarget -match '^gfx\d{3}X$') {
return @(
"$GpuTarget-all",
"$GpuTarget-dgpu"
)
}
throw "Invalid GPU target format: $GpuTarget"
}
# Test S3 prefix existence using existing Invoke-WebRequest pattern
function Test-S3TargetExists {
param(
[string]$BaseUrl,
[string]$Prefix
)
$url = "$BaseUrl/?prefix=$Prefix"
try {
$response = Invoke-WebRequest -Uri $url -UseBasicParsing
return $response.Content -match '<Key>.+</Key>'
}
catch {
return $false
}
}
function Resolve-S3Target {
param(
[string]$GpuTarget,
[string]$BaseUrl
)
$candidates = Get-S3TargetCandidates $GpuTarget
foreach ($target in $candidates) {
$prefix = "therock-dist-windows-$target"
if (Test-S3TargetExists $BaseUrl $prefix) {
if ($VerboseLogging) {
Write-Log "Selected S3 target: $target" "DEBUG"
}
return $target
}
}
throw "No valid S3 target found for: $GpuTarget"
}
function Detect-GpuInfo {
if ($VerboseLogging) {
Write-Log "Detecting GPU via WMI..." "DEBUG"
}
$gpuList = @()
try {
$gpuList = Get-CimInstance Win32_VideoController |
Where-Object { $_.Name -match "AMD|Radeon" }
} catch {
Write-Log "WMI detection failed: $_" "WARN"
}
if (-not $gpuList -or $gpuList.Count -eq 0) {
Write-Log "No AMD GPU detected, using fallback: $($Config.FallbackGfxTarget)" "WARN"
return [PSCustomObject]@{
Gfx = $Config.FallbackGfxTarget
Name = "Unknown AMD GPU"
}
}
$gpu = $gpuList[0]
$name = $gpu.Name
if ($VerboseLogging) {
Write-Log "Detected GPU: $name" "DEBUG"
}
$normalized = $name.ToLower()
$gfx = switch -Regex ($normalized) {
"7900" { "gfx1100" }
"7800" { "gfx1101" }
"7700" { "gfx1103" }
"7600" { "gfx1102" }
"6950|6900|6800" { "gfx1030" }
"6700|6600" { "gfx1031" }
"5700" { "gfx1031" }
"vega" { "gfx900" }
default { $null }
}
if (-not $gfx) {
Write-Log "Could not map GPU automatically, using fallback: $($Config.FallbackGfxTarget)" "WARN"
$gfx = $Config.FallbackGfxTarget
}
if ($VerboseLogging) {
Write-Log "Mapped GFX target: $gfx" "DEBUG"
}
return [PSCustomObject]@{
Gfx = $gfx
Name = $name
}
}
function Get-LatestROCmVersion {
param([string]$S3Target)
Write-Log "Auto-detecting latest ROCm version for target: $S3Target" "DEBUG"
try {
if (-not $S3Target) {
throw "S3Target is empty. Cannot query ROCm versions."
}
$s3Url = "$($Config.RocmS3BaseUrl)/?prefix=therock-dist-windows-$S3Target"
$response = Invoke-WebRequest -Uri $s3Url -UseBasicParsing
$content = $response.Content
$files = [regex]::Matches($content, '<Key>([^<]+)</Key>') | ForEach-Object {
$_.Groups[1].Value
}
if (-not $files -or $files.Count -eq 0) {
throw "No files returned from S3 listing"
}
# Filter valid tarballs
$versionFiles = foreach ($file in $files) {
if ($file -match "therock-dist-windows-${S3Target}-.*?(\d+\.\d+\.\d+(?:a|rc)\d+)\.tar\.gz"){
$version = $matches[1]
if ($version -match '(\d+)\.(\d+)\.(\d+)(?:(a|rc)(\d+))?') {
[PSCustomObject]@{
File = $file
Version = $version
Major = [int]$matches[1]
Minor = [int]$matches[2]
Patch = [int]$matches[3]
Type = if ($matches[4]) { $matches[4] } else { "release" }
RC = if ($matches[5]) { [int]$matches[5] } else { 0 }
IsAlpha = $version -match 'a'
}
}
}
}
if (-not $versionFiles) {
throw "No matching ROCm tarballs found for target $S3Target"
}
# Sort versions properly
$latest = $versionFiles |
Sort-Object Major, Minor, Patch, @{Expression={if($_.IsAlpha){1}else{0}}}, RC -Descending |
Select-Object -First 1
Write-Log "Latest ROCm version detected: $($latest.Version)" "SUCCESS"
return @{
Version = $latest.Version
Url = "$($Config.RocmS3BaseUrl)/$($latest.File)"
}
}
catch {
Write-Log "Failed to auto-detect ROCm version: $($_.Exception.Message)" "ERROR"
exit 1
}
}
function Download-ROCm {
param(
[string]$Version,
[string]$S3Target,
[bool]$ReuseExisting = $true
)
$rocmTarball = "rocm.tar.gz"
# Check if tarball already exists
if ($ReuseExisting -and (Test-Path $rocmTarball)) {
if ($VerboseLogging) {
Write-Log "Found existing ROCm tarball: $rocmTarball" "DEBUG"
Write-Log "Skipping download (use -KeepROCmTarball:`$false to force re-download)" "DEBUG"
}
# Try to determine version from tarball name or use provided version
$detectedVersion = if ($Version -eq "latest") {
$rocmInfo = Get-LatestROCmVersion -S3Target $S3Target
$rocmInfo.Version
} else {
$Version
}
return $detectedVersion
}
if ($VerboseLogging) {
Write-Log "Downloading ROCm $Version..." "DEBUG"
}
$rocmInfo = if ($Version -eq "latest") {
Get-LatestROCmVersion -S3Target $S3Target
} else {
@{
Version = $Version
Url = "$($Config.RocmS3BaseUrl)/therock-dist-windows-${S3Target}-${Version}.tar.gz"
}
}
if ($VerboseLogging) {
Write-Log "Downloading from: $($rocmInfo.Url)" "DEBUG"
}
try {
# Use System.Net.WebClient for better progress reporting
$webClient = New-Object System.Net.WebClient
Register-ObjectEvent -InputObject $webClient -EventName DownloadProgressChanged -Action {
$percent = $EventArgs.ProgressPercentage
Write-Progress -Activity "Downloading ROCm" -Status "$percent% Complete" -PercentComplete $percent
} | Out-Null
$webClient.DownloadFile($rocmInfo.Url, (Resolve-Path .).Path + "\$rocmTarball")
$webClient.Dispose()
Write-Progress -Activity "Downloading ROCm" -Completed
if ($VerboseLogging) {
Write-Log "ROCm tarball downloaded successfully" "DEBUG"
}
return $rocmInfo.Version
}
catch {
Write-Log "Failed to download ROCm: $_" "ERROR"
exit 1
}
}
function Extract-ROCm {
param(
[bool]$ReuseExisting = $true
)
# Check if ROCm is already extracted and valid
if ($ReuseExisting -and (Test-Path $Config.RocmInstallPath)) {
$clangPath = Join-Path $Config.RocmInstallPath "lib\llvm\bin\clang.exe"
if (Test-Path $clangPath) {
if ($VerboseLogging) {
Write-Log "Found existing ROCm installation at: $($Config.RocmInstallPath)" "DEBUG"
Write-Log "Skipping extraction (use -ReuseROCm:`$false to force re-extract)" "DEBUG"
}
return
} else {
Write-Log "Existing ROCm installation appears incomplete, re-extracting..." "WARN"
Remove-Item -Recurse -Force $Config.RocmInstallPath
}
}
if ($VerboseLogging) {
Write-Log "Extracting ROCm to $Config.RocmInstallPath..." "DEBUG"
}
# Create directory if it doesn't exist
New-Item -ItemType Directory -Force -Path $Config.RocmInstallPath | Out-Null
# Extract tarball using tar (built-in on Windows 10+)
$result = tar -xzf "rocm.tar.gz" -C $Config.RocmInstallPath --strip-components=1 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to extract ROCm: $result" "ERROR"
exit 1
}
if ($VerboseLogging) {
Write-Log "ROCm extracted successfully" "DEBUG"
}
}
function Clone-TurboQuant {
param(
[bool]$ReuseExisting = $true
)
$repoPath = "llama.cpp"
# Check if repository already exists
if ($ReuseExisting -and (Test-Path $repoPath)) {
if ($VerboseLogging) {
Write-Log "Found existing llama.cpp repository" "DEBUG"
}
Push-Location $repoPath
# Check if it's a git repository
$isGitRepo = Test-Path ".git"
if ($isGitRepo) {
if ($VerboseLogging) {
Write-Log "Checking repository status..." "DEBUG"
}
# Get current branch
$currentBranch = git rev-parse --abbrev-ref HEAD 2>&1
if ($currentBranch -eq $Config.RepoBranch) {
if ($VerboseLogging) {
Write-Log "Already on branch: $($Config.RepoBranch)" "DEBUG"
Write-Log "Fetching latest commit..." "DEBUG"
}
# 🔧 Always fetch latest state (works with shallow clones)
git fetch origin $Config.RepoBranch --depth=1 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to fetch branch" "ERROR"
Pop-Location
exit 1
}
git reset --hard FETCH_HEAD 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to reset repository to fetched commit" "ERROR"
Pop-Location
exit 1
}
if ($VerboseLogging) {
Write-Log "Repository updated to latest commit (FETCH_HEAD)" "DEBUG"
}
} else {
if ($VerboseLogging) {
Write-Log "Repository is on branch '$currentBranch', switching to '$($Config.RepoBranch)'..." "DEBUG"
}
# Fetch the latest from the target branch first
git fetch origin $Config.RepoBranch 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to fetch branch" "ERROR"
Pop-Location
exit 1
}
git checkout $Config.RepoBranch --force 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to switch branch" "ERROR"
Pop-Location
exit 1
}
}
# Get the current commit hash after reset/pull
$commitHash = git rev-parse --short=7 HEAD
$commitDate = git log -1 --format=%cd --date=short
Pop-Location
if ($VerboseLogging) {
Write-Log "Using repository (commit: $commitHash, date: $commitDate)" "DEBUG"
}
return $commitHash
} else {
Pop-Location
if ($VerboseLogging) {
Write-Log "Directory exists but is not a git repository, removing..." "DEBUG"
}
Remove-DirectorySafe -Path $repoPath
}
}
if ($VerboseLogging) {
Write-Log "Cloning TurboQuant llama.cpp fork..." "DEBUG"
}
try {
$gitOutput = git clone --quiet --depth 1 --single-branch --branch $Config.RepoBranch $Config.RepoUrl "llama.cpp" 2>&1
if ($LASTEXITCODE -ne 0) {
throw "git clone failed: $gitOutput"
}
Write-Verbose "Repository cloned successfully"
} catch {
Write-Log "Failed to clone TurboQuant repository" "ERROR"
Write-Error $_
exit 1
}
Push-Location $repoPath
$commitHash = git rev-parse --short=7 HEAD 2>$null
$commitHash = $commitHash.Trim()
Pop-Location
if ($VerboseLogging) {
Write-Log "TurboQuant cloned successfully (commit: $commitHash)" "DEBUG"
}
return $commitHash
}
function Apply-Patches {
if ($VerboseLogging) {
Write-Log "Checking for patches to apply..." "DEBUG"
}
# Store the original directory (before entering llama.cpp)
$originalDir = Get-Location
$patchDir = Join-Path $originalDir "patches"
if (-not (Test-Path $patchDir)) {
if ($VerboseLogging) {
Write-Log "No patches directory found at: $patchDir" "DEBUG"
Write-Log "Skipping patch application" "DEBUG"
}
return
}
$patchFiles = Get-ChildItem -Path $patchDir -Filter "*.patch" -File | Sort-Object Name
if ($patchFiles.Count -eq 0) {
if ($VerboseLogging) {
Write-Log "No patch files found in $patchDir" "DEBUG"
}
return
}
if ($VerboseLogging) {
Write-Log "Found $($patchFiles.Count) patch file(s) to apply" "DEBUG"
}
Push-Location "llama.cpp"
foreach ($patchFile in $patchFiles) {
Write-ColorOutput " Applying: $($patchFile.Name)" "Info"
# Use full path to patch file
$patchPath = $patchFile.FullName
# Try to apply the patch
$result = git apply "$patchPath" 2>&1
if ($LASTEXITCODE -eq 0) {
if ($VerboseLogging) {
Write-Log " ✓ Successfully applied: $($patchFile.Name)" "DEBUG"
}
} else {
# Check if patch is already applied
$checkResult = git apply --check "$patchPath" 2>&1
if ($checkResult -match "already exists in working directory" -or
$checkResult -match "patch does not apply") {
Write-Log " ⚠ Patch already applied or not needed: $($patchFile.Name)" "WARN"
} else {
Write-Log " ✗ Failed to apply patch: $($patchFile.Name)" "ERROR"
Write-Log " Error: $result" "ERROR"
Pop-Location
exit 1
}
}
}
Pop-Location
if ($VerboseLogging) {
Write-Log "Patch application completed" "DEBUG"
}
}
function Build-LlamaCpp {
param(
[string]$MappedTargets
)
# Set up Visual Studio environment
if ($VerboseLogging) {
Write-Log "Setting up Visual Studio environment..." "DEBUG"
}
$vsPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if (-not $vsPath) {
Write-Log "Visual Studio installation not found" "ERROR"
exit 1
}
$vcVarsPath = Join-Path $vsPath "VC\Auxiliary\Build\vcvars64.bat"
if (-not (Test-Path $vcVarsPath)) {
Write-Log "vcvars64.bat not found at: $vcVarsPath" "ERROR"
exit 1
}
# Set environment variables for ROCm
$env:HIP_PATH = $Config.RocmInstallPath
$env:HIP_PLATFORM = "amd"
$env:PATH = "$($Config.RocmInstallPath)\lib\llvm\bin;$($Config.RocmInstallPath)\bin;$env:PATH"
# Create build directory
Push-Location "llama.cpp"
New-Item -ItemType Directory -Force -Path "build" | Out-Null
Push-Location "build"
# CMake configure
$cmakeArgs = @(
"..",
"-G", $Config.CMakeGenerator,
"-DCMAKE_C_COMPILER=$($Config.RocmInstallPath)\lib\llvm\bin\clang.exe",
"-DCMAKE_CXX_COMPILER=$($Config.RocmInstallPath)\lib\llvm\bin\clang++.exe",
"-DCMAKE_CXX_FLAGS=-I$($Config.RocmInstallPath)\include",
"-DCMAKE_CROSSCOMPILING=ON",
"-DCMAKE_BUILD_TYPE=$($Config.CMakeBuildType)",
"-DGPU_TARGETS=$MappedTargets",
"-DBUILD_SHARED_LIBS=ON",
"-DLLAMA_BUILD_TESTS=OFF",
"-DGGML_HIP=ON",
"-DGGML_OPENMP=OFF",
"-DGGML_CUDA_FORCE_CUBLAS=OFF",
"-DGGML_RPC=ON",
"-DGGML_HIP_ROCWMMA_FATTN=OFF",
"-DLLAMA_BUILD_BORINGSSL=ON",
"-DGGML_NATIVE=OFF",
"-DGGML_STATIC=OFF",
"-DCMAKE_SYSTEM_NAME=Windows"
)
# Run CMake configure with vcvars environment
if ($VerboseLogging) {
Write-Log "Running CMake configuration..." "DEBUG"
}
$cmakeCmd = "call `"$vcVarsPath`" && cmake $($cmakeArgs -join ' ')"
$result = cmd /c $cmakeCmd 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Log "CMake configuration failed" "ERROR"
if ($result) {
Write-Host "`nCMake output:" -ForegroundColor Red
Write-Host ($result -join "`n") -ForegroundColor Red
}
Pop-Location
Pop-Location
exit 1
}
if ($VerboseLogging) {
Write-Log "CMake configuration completed successfully" "DEBUG"
Write-Log "Building llama.cpp with Ninja..." "DEBUG"
}
# Build
if ($VerboseLogging) {
Write-Log "Starting build process..." "DEBUG"
}
$buildCmd = "call `"$vcVarsPath`" && cmake --build . -j $env:NUMBER_OF_PROCESSORS"
$result = cmd /c $buildCmd 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Log "Build failed" "ERROR"
if ($result) {
Write-Host "`nBuild output:" -ForegroundColor Red
Write-Host ($result -join "`n") -ForegroundColor Red
}
Pop-Location
Pop-Location
exit 1
}
Pop-Location
Pop-Location
if ($VerboseLogging) {
Write-Log "Build completed successfully" "DEBUG"
}
}
function Copy-ROCmLibraries {
if ($VerboseLogging) {
Write-Log "Copying ROCm runtime libraries to build directory..." "DEBUG"
}
$buildBinPath = "llama.cpp\build\bin"
$rocmBinPath = "$($Config.RocmInstallPath)\bin"
if (-not (Test-Path $rocmBinPath)) {
Write-Log "ROCm bin directory not found: $rocmBinPath" "ERROR"
exit 1
}
# List of DLL patterns to copy
$dllPatterns = @(
"amdhip64_*.dll",
"rocm_kpack.dll",
"amd_comgr*.dll",
"libhipblas.dll",
"rocblas.dll",
"rocsolver.dll",
"hipblaslt.dll",
"libhipblaslt.dll",
"hipblas.dll"
)
foreach ($pattern in $dllPatterns) {
$matchingFiles = Get-ChildItem -Path $rocmBinPath -Filter $pattern -ErrorAction SilentlyContinue
if ($matchingFiles) {
foreach ($file in $matchingFiles) {
Copy-Item $file.FullName -Destination $buildBinPath -Force
if ($VerboseLogging) {
Write-Log " Copied: $($file.Name)" "DEBUG"
}
}
} else {
Write-Log " Warning: No files found matching pattern: $pattern" "WARN"
}
}
# Copy rocblas/library folder
$rocblasLibPath = Join-Path $rocmBinPath "rocblas\library"
if (Test-Path $rocblasLibPath) {
if ($VerboseLogging) {
Write-Log " Copying rocblas\library folder..." "DEBUG"
}
$destRocblasPath = Join-Path $buildBinPath "rocblas\library"
Copy-Item -Path $rocblasLibPath -Destination $destRocblasPath -Recurse -Force
if ($VerboseLogging) {
Write-Log " Copied rocblas\library folder" "DEBUG"
}
} else {
Write-Log " Warning: rocblas\library folder not found at: $rocblasLibPath" "WARN"
}
# Copy hipblaslt/library folder
$hipblasltLibPath = Join-Path $rocmBinPath "hipblaslt\library"
if (Test-Path $hipblasltLibPath) {
if ($VerboseLogging) {
Write-Log " Copying hipblaslt\library folder..." "DEBUG"
}
$destHipblasltPath = Join-Path $buildBinPath "hipblaslt\library"
Copy-Item -Path $hipblasltLibPath -Destination $destHipblasltPath -Recurse -Force
if ($VerboseLogging) {
Write-Log " Copied hipblaslt\library folder" "DEBUG"
}
} else {
Write-Log " Warning: hipblaslt\library folder not found at: $hipblasltLibPath" "WARN"
}
if ($VerboseLogging) {
Write-Log "ROCm libraries copied successfully" "DEBUG"
}
}
function New-BuildSummary {
param(
[string]$ROCmVersion,
[string]$CommitHash,
[string]$GfxTarget
)
$summaryPath = "build-summary.txt"
$summary = @"
================================================================================
TurboQuant llama.cpp + ROCm Build Summary
================================================================================
Build Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Platform: Windows
GPU Target: $GfxTarget ($($Global:GpuInfoCache.Name))
ROCm Version: $ROCmVersion
TurboQuant Commit: $CommitHash
TurboQuant Branch: $($Config.RepoBranch)
Build Type: $($Config.CMakeBuildType)
CMake Generator: $($Config.CMakeGenerator)
Build Configuration:
HIP Support: Enabled
RPC Support: Enabled
Shared Libraries: Yes
Native Opt: Disabled (for portability)
OpenMP: Disabled
ROCm WMMA: Disabled
Build Location:
Source: $(Resolve-Path "llama.cpp")
Binaries: $(Resolve-Path "llama.cpp\build\bin")
ROCm Installation:
Path: $($Config.RocmInstallPath)
TurboQuant Features:
TurboQuant provides extreme KV cache quantization (up to 4.57x compression)
for efficient inference with llama.cpp. This fork includes optimizations
specifically for memory-constrained scenarios.
Branch: $($Config.RepoBranch)
More info: https://github.com/TheTom/llama-cpp-turboquant
Usage Example:
cd llama.cpp\build\bin
.\llama-server.exe -m model.gguf -ngl 99
Next Steps:
1. Copy the contents of llama.cpp\build\bin to your desired location
2. All ROCm runtime libraries are included - no separate ROCm installation needed
3. Test with: llama-cli.exe -m <model.gguf> -p "Hello world" -n 50
================================================================================
"@
Set-Content -Path $summaryPath -Value $summary
Write-ColorOutput "Build summary saved to: $summaryPath" "Info"
# Also display summary
Write-Host ""
Write-Host $summary -ForegroundColor Green
}
function Remove-DirectorySafe {
param(
[Parameter(Mandatory)]
[string]$Path,
[int]$MaxRetries = 3,
[int]$DelaySeconds = 2
)
if (-not (Test-Path $Path)) {
Write-Verbose "Directory does not exist: $Path"
return
}
for ($i = 1; $i -le $MaxRetries; $i++) {
try {
Write-Verbose "Attempting to remove: $Path (try $i/$MaxRetries)"
Remove-Item -Recurse -Force $Path -ErrorAction Stop
Write-Verbose "Successfully removed: $Path"
return
}
catch {
Write-Log "Failed to remove ${Path}: $($_.Exception.Message)" "WARN"
if ($i -eq $MaxRetries) {
throw "Cleanup failed after $MaxRetries attempts. File likely locked.`nPath: $Path"
}
Start-Sleep -Seconds $DelaySeconds
}
}
}
function Get-LockingProcessHint {
param([string]$Path)
Write-Warning "File lock detected. Common causes:"
Write-Warning "- CMake still running"
Write-Warning "- Ninja build in background"
Write-Warning "- VSCode terminal holding handle"
Write-Warning "- Explorer open in that folder"
Write-Warning "Try closing related processes or rebooting shell."
}
function Cleanup {
param(
[bool]$KeepROCmInstallation = $true,
[bool]$KeepTarball = $true
)
if ($VerboseLogging) {
Write-Log "Cleaning up temporary files..." "DEBUG"
}
# Remove ROCm tarball with proper error handling
if (-not $KeepTarball -and (Test-Path "rocm.tar.gz")) {
try {
Remove-Item "rocm.tar.gz" -Force -ErrorAction Stop
if ($VerboseLogging) {
Write-Log " Removed rocm.tar.gz" "DEBUG"
}
} catch {
Write-Warning "Failed to remove rocm.tar.gz: $($_.Exception.Message)"
}
} elseif (Test-Path "rocm.tar.gz") {
if ($VerboseLogging) {
Write-Log " Keeping rocm.tar.gz for future use" "DEBUG"
}
}
# Remove ROCm installation with proper error handling
if (-not $KeepROCmInstallation -and (Test-Path $Config.RocmInstallPath)) {
try {
Write-Verbose "Removing ROCm installation..."
Remove-DirectorySafe -Path $Config.RocmInstallPath
if ($VerboseLogging) {
Write-Log " Removed ROCm installation" "DEBUG"
}
} catch {
Get-LockingProcessHint -Path $Config.RocmInstallPath
throw $_
}
} else {
if ($VerboseLogging) {
Write-Log " Keeping ROCm installation at $($Config.RocmInstallPath)" "DEBUG"
}
}
if (-not $CleanBuild -and (Test-Path "llama.cpp")) {
try {
Write-Verbose "Removing existing llama.cpp directory..."
Remove-DirectorySafe -Path "llama.cpp"
if ($VerboseLogging) {
Write-Log " Removed llama.cpp directory" "DEBUG"
}
} catch {
Get-LockingProcessHint -Path "llama.cpp"
throw $_
}
}
if ($VerboseLogging) {
Write-Log "Cleanup completed" "DEBUG"
}
}
# ============================================================================
# Main Build Process
# ============================================================================
function Invoke-BuildPipeline {
$context = Initialize-BuildContext -GfxTarget $GfxTarget -ROCmVersion $ROCmVersion
Write-Host ""
Write-Host "============================================================================" -ForegroundColor Cyan
Write-Host " TurboQuant llama.cpp + ROCm Build Script" -ForegroundColor Cyan
Write-Host "============================================================================" -ForegroundColor Cyan
Write-Host ""
# STEP 0: GPU detection FIRST
if ($DetectGpu) {
$Global:GpuInfoCache = Detect-GpuInfo
if ($Global:GpuInfoCache.Gfx) {
$GfxTarget = $Global:GpuInfoCache.Gfx
$GPU_EXAMPLE = $Global:GpuInfoCache.Name
}
}
try {
# Step 2.5: Detect GPU if enabled
if ($DetectGpuOnly) {
Write-Step "GPU Detection Result" @(
"GPU: $($Global:GpuInfoCache.Name)",
"GFX: $($Global:GpuInfoCache.Gfx)"
)
exit 0
}
# Step 2.6: Normalize GFX target from detected GPU
if ($DetectGpu -and $Global:GpuInfoCache.Gfx) {
Write-Step "GPU Architecture Detection" @(
"Detected GPU: $($Global:GpuInfoCache.Name)",
"Raw architecture: $($Global:GpuInfoCache.Gfx)"
)
$S3Target = Get-GfxTargetFromArchitecture $Global:GpuInfoCache.Gfx
if (-not $S3Target) {
throw "Unsupported or undetected GPU architecture: $($Global:GpuInfoCache.Gfx)"
}
# Update context with normalized target
$context = Initialize-BuildContext -GfxTarget $S3Target -ROCmVersion $ROCmVersion
$context = Set-S3Target $context
# 🔧 Use clean GFX family for compilation
$cleanGfx = $context.GfxTarget -replace "-all|-dgpu", ""
$context.MappedTargets = Get-MappedGfxTargets -GfxTarget $cleanGfx
}
# Step 1: Clean existing directories if requested
if ($CleanBuild) {
Write-Log "Cleanup: removing existing directories" "STEP"
try {
if (Test-Path "llama.cpp") {
Write-Verbose "Removing existing llama.cpp directory..."
Remove-DirectorySafe -Path "llama.cpp"
}
if (-not $KeepROCmTarball -and (Test-Path "rocm.tar.gz")) {
Write-Verbose "Removing ROCm tarball..."
Remove-Item -Force "rocm.tar.gz" -ErrorAction Stop
}
Write-Verbose "Cleanup completed successfully"
} catch {
Get-LockingProcessHint -Path "llama.cpp"
Write-Log "Cleanup failed: $($_.Exception.Message)" "ERROR"
exit 1
}
Write-Host ""
} else {
Write-Log "Cleanup: skipped (incremental)" "STEP"
Write-Host ""
}
# Step 2: Install prerequisites
if (-not $SkipPrerequisites) {
Write-Log "Prerequisites: installing" "STEP"
Install-Prerequisites
Write-Host ""
} else {
Write-Log "Prerequisites: skipped" "STEP"
Write-Host ""
}
# Step 3: Determine ROCm S3 target
if (-not $DetectGpuOnly) {
Write-Step "ROCm Setup" @(
"S3 Target: $($context.S3Target)",
"Version: $($context.ROCmVersion)"
)
}
Write-Host ""
# Step 4: Download ROCm
if (-not $DetectGpuOnly) {
Write-Log "ROCm: downloading" "STEP"
$context = Invoke-DownloadROCm $context
}
Write-Host ""
# Step 5: Extract ROCm
if (-not $DetectGpuOnly) {
Write-Log "ROCm: extracting" "STEP"
Extract-ROCm -ReuseExisting $KeepROCmTarball
}
Write-Host ""
# Step 6: Validate and clone TurboQuant
if (-not $DetectGpuOnly) {
Write-Log "Repository: setting up" "STEP"
# Validate that cleanup was successful
if ($CleanBuild -and (Test-Path "llama.cpp")) {
Write-Log "Target directory 'llama.cpp' still exists after cleanup. Aborting clone." "ERROR"
exit 1
}
$commitHash = Clone-TurboQuant -ReuseExisting (-not $CleanBuild)
}
Write-Host ""
# Step 6.5: Apply patches
if (-not $DetectGpuOnly) {
Write-Log "Patches: applying" "STEP"
Apply-Patches
}
Write-Host ""
# Step 7: Build llama.cpp
if (-not $DetectGpuOnly) {
Write-Step "Build Configuration" @(
"GPU Targets: $($context.MappedTargets)",
"Generator: $($Config.CMakeGenerator)",
"Type: $($Config.CMakeBuildType)"
)
Build-LlamaCpp -MappedTargets $context.MappedTargets
}
Write-Host ""
# Step 8: Copy ROCm libraries
if (-not $DetectGpuOnly) {
Write-Log "Libraries: copying ROCm runtime" "STEP"
Copy-ROCmLibraries
}
Write-Host ""
# Step 9: Create build summary
if (-not $DetectGpuOnly) {
Write-Log "Summary: creating" "STEP"
New-BuildSummary `
-ROCmVersion $context.RocmInfo.Version `
-CommitHash $commitHash `
-GfxTarget $context.GfxTarget
}
Write-Host ""
# Step 10: Cleanup
if (-not $DetectGpuOnly) {
Write-Log "Cleanup: removing temporary files" "STEP"
Cleanup -KeepROCmInstallation $true -KeepTarball $KeepROCmTarball
}
Write-Host ""
if (-not $DetectGpuOnly) {
Write-Log "============================================================================" "SUCCESS"
Write-Log "Build complete | Binaries: llama.cpp\build\bin | All ROCm libraries included" "SUCCESS"
Write-Log "============================================================================" "SUCCESS"
} else {
Write-Log "GPU detection complete" "SUCCESS"
Write-Host ""
}
Write-Host ""
}
catch {
Write-Log "============================================================================" "ERROR"
$errorMsg = if ($_.Exception -and $_.Exception.Message) {
$_.Exception.Message
} else {
$_ | Out-String
}
Write-Log "Build failed: $errorMsg" "ERROR"
Write-Log "============================================================================" "ERROR"
# Show full error details for debugging
Write-Host "`nFull Error Details:" -ForegroundColor Red
try {
Write-Host ($_ | Format-List -Force | Out-String) -ForegroundColor Red
} catch {
Write-Host $_ -ForegroundColor Red
}
exit 1
}
}
# ============================================================================
# Script Entry Point
# ============================================================================
Invoke-BuildPipeline
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment