include llama.cpp since there were some edits

This commit is contained in:
zefie committed 2026-11-01 16:32:35 -05:00
1 parent db7f28d087
commit 8c66990853
3918 files changed
+1640170 -1

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
./scripts/apple/validate-ios.sh
./scripts/apple/validate-macos.sh
./scripts/apple/validate-visionos.sh
./scripts/apple/validate-tvos.sh
+820
View File
@@ -0,0 +1,820 @@
#!/usr/bin/env bash
# validate-ios.sh - Validate iOS Application with embedded llama.xcframework using SwiftUI
# Authentication options (optional) (can be set via environment variables)
# To use: export APPLE_ID=your.email@example.com
# export APPLE_PASSWORD=your-app-specific-password
# ./validate-ios.sh
APPLE_ID=${APPLE_ID:-""}
APPLE_PASSWORD=${APPLE_PASSWORD:-""}
# Ensure the script exits on error
set -e
# Function to print usage instructions
print_usage() {
echo "Usage: ./validate-ios.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --help Show this help message"
echo " --apple-id EMAIL Apple ID email for validation"
echo " --apple-password PWD App-specific password for Apple ID"
echo ""
echo "Environment variables:"
echo " APPLE_ID Apple ID email for validation"
echo " APPLE_PASSWORD App-specific password for Apple ID"
echo ""
echo "Notes:"
echo " - Command line options take precedence over environment variables"
echo " - Authentication is optional. If not provided, alternative validation will be performed"
echo " - For APPLE_PASSWORD, use an app-specific password generated at https://appleid.apple.com/account/manage"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help)
print_usage
exit 0
;;
--apple-id)
APPLE_ID="$2"
shift 2
;;
--apple-password)
APPLE_PASSWORD="$2"
shift 2
;;
*)
echo "Unknown option: $1"
print_usage
exit 1
;;
esac
done
# Function to clean up in case of error
cleanup() {
# Don't clean up temp files on error to help with debugging
echo "===== iOS Validation Process Failed ====="
exit 1
}
# Set up trap to call cleanup function on error
trap cleanup ERR
set -e # Exit on any error
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
BUILD_DIR="${ROOT_DIR}/validation-builds/ios"
# Configuration
APP_NAME="iOSLlamaTest"
BUNDLE_ID="org.ggml.iOSLlamaTest"
XCFRAMEWORK_PATH="${ROOT_DIR}/build-apple/llama.xcframework"
TEMP_DIR="${BUILD_DIR}/temp"
ARCHIVE_PATH="${BUILD_DIR}/${APP_NAME}.xcarchive"
IPA_PATH="${BUILD_DIR}/${APP_NAME}.ipa"
VALIDATION_DIR="${BUILD_DIR}/validation"
# Create necessary directories
mkdir -p "${BUILD_DIR}"
mkdir -p "${TEMP_DIR}"
mkdir -p "${VALIDATION_DIR}"
echo "===== iOS Validation Process Started ====="
# 1. Create a simple test app project
echo "Creating test iOS app project..."
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>
EOF
# Create SwiftUI app files
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources"
# Create App.swift
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/App.swift" << EOF
import SwiftUI
import llama
@main
struct LlamaTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
EOF
# Create ContentView.swift
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/ContentView.swift" << EOF
import SwiftUI
import llama
struct ContentView: View {
// Test that we can initialize a llama context params struct
let params = llama_context_default_params()
var body: some View {
VStack(spacing: 20) {
Text("Llama Framework Test")
.font(.largeTitle)
.padding()
Text("llama_context_default_params() created successfully")
.font(.headline)
.multilineTextAlignment(.center)
.padding()
// Display some param values to confirm the framework is working
Text("n_ctx: \(params.n_ctx)")
.font(.body)
Text("n_batch: \(params.n_batch)")
.font(.body)
Spacer()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EOF
# Create project.pbxproj, fixing the framework search paths issues
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
11111111111111111111111 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22222222222222222222222; };
33333333333333333333333 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44444444444444444444444; };
55555555555555555555555 /* llama.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
77777777777777777777777 /* llama.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
88888888888888888888888 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
77777777777777777777777 /* llama.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
99999999999999999999999 /* ${APP_NAME}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "${APP_NAME}.app"; sourceTree = BUILT_PRODUCTS_DIR; };
22222222222222222222222 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
44444444444444444444444 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
66666666666666666666666 /* llama.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = llama.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */
EOF
# Add the rest of the project file with fixed framework search paths
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXFrameworksBuildPhase section */
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
55555555555555555555555 /* llama.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */ = {
isa = PBXGroup;
children = (
99999999999999999999999 /* ${APP_NAME}.app */,
);
name = Products;
sourceTree = "<group>";
};
EOF
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */ = {
isa = PBXGroup;
children = (
66666666666666666666666 /* llama.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
EEEEEEEEEEEEEEEEEEEEEEEE = {
isa = PBXGroup;
children = (
FFFFFFFFFFFFFFFFFFFFFFFF /* iOSLlamaTest */,
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */,
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */,
);
sourceTree = "<group>";
};
FFFFFFFFFFFFFFFFFFFFFFFF /* iOSLlamaTest */ = {
isa = PBXGroup;
children = (
1111111111111111111111AA /* Sources */,
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */,
);
path = "iOSLlamaTest";
sourceTree = "<group>";
};
1111111111111111111111AA /* Sources */ = {
isa = PBXGroup;
children = (
22222222222222222222222 /* App.swift */,
44444444444444444444444 /* ContentView.swift */,
);
path = Sources;
sourceTree = "<group>";
};
/* End PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin PBXNativeTarget section */
3333333333333333333333AA /* ${APP_NAME} */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */;
buildPhases = (
5555555555555555555555AA /* Sources */,
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */,
6666666666666666666666AA /* Resources */,
88888888888888888888888 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "${APP_NAME}";
productName = "${APP_NAME}";
productReference = 99999999999999999999999 /* ${APP_NAME}.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7777777777777777777777AA /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1240;
TargetAttributes = {
3333333333333333333333AA = {
CreatedOnToolsVersion = 12.4;
};
};
};
buildConfigurationList = 8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = EEEEEEEEEEEEEEEEEEEEEEEE;
productRefGroup = CCCCCCCCCCCCCCCCCCCCCCCC /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3333333333333333333333AA /* ${APP_NAME} */,
);
};
/* End PBXProject section */
EOF
# Add the rest of the file with correct FRAMEWORK_SEARCH_PATHS
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXResourcesBuildPhase section */
6666666666666666666666AA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
5555555555555555555555AA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33333333333333333333333 /* ContentView.swift in Sources */,
11111111111111111111111 /* App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
9999999999999999999999AA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
AAAAAAAAAAAAAAAAAAAAABBB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)";
INFOPLIST_FILE = "iOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.iOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = "iOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.iOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
EOF
# Finish the project.pbxproj file
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin XCConfigurationList section */
8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9999999999999999999999AA /* Debug */,
AAAAAAAAAAAAAAAAAAAAABBB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */,
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7777777777777777777777AA /* Project object */;
}
EOF
# 2. Copy XCFramework to test project
echo "Copying XCFramework to test project..."
cp -R "${XCFRAMEWORK_PATH}" "${TEMP_DIR}/${APP_NAME}/"
# 3. Build and archive the app
echo "Building and archiving test app..."
cd "${TEMP_DIR}/${APP_NAME}"
# Create a simple xcscheme file to avoid xcodebuild scheme issues
mkdir -p "${APP_NAME}.xcodeproj/xcshareddata/xcschemes"
cat > "${APP_NAME}.xcodeproj/xcshareddata/xcschemes/${APP_NAME}.xcscheme" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
EOF
# Now use xcodebuild with an explicitly defined product name
xcodebuild -project "${APP_NAME}.xcodeproj" -scheme "${APP_NAME}" -sdk iphoneos -configuration Release archive -archivePath "${ARCHIVE_PATH}" CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO PRODUCT_NAME="${APP_NAME}" SWIFT_OPTIMIZATION_LEVEL="-Onone" -quiet
# 4. Create IPA from archive
echo "Creating IPA from archive..."
mkdir -p "${TEMP_DIR}/Payload"
cp -R "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" "${TEMP_DIR}/Payload/"
# Check and log app structure before zipping
echo "App structure:"
ls -la "${TEMP_DIR}/Payload/${APP_NAME}.app/"
echo "Frameworks:"
ls -la "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
cd "${TEMP_DIR}"
zip -r "${IPA_PATH}" Payload
# Check embedded provisioning profile
echo "Checking provisioning profile (if any)..."
PROVISIONING_PROFILE=$(find "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" -name "embedded.mobileprovision" 2>/dev/null)
if [ -n "$PROVISIONING_PROFILE" ]; then
echo "Found embedded provisioning profile:"
security cms -D -i "$PROVISIONING_PROFILE" || echo "Unable to decode provisioning profile"
else
echo "No embedded provisioning profile found (expected for ad-hoc builds)"
fi
# 5. Validate the IPA
echo "Validating IPA..."
VALIDATION_OUTPUT="${VALIDATION_DIR}/validation_output.txt"
# Check if authentication credentials are provided
AUTH_ARGS=""
if [ -n "$APPLE_ID" ] && [ -n "$APPLE_PASSWORD" ]; then
echo "Using Apple ID authentication for validation..."
AUTH_ARGS="--username \"$APPLE_ID\" --password \"$APPLE_PASSWORD\""
else
echo "No authentication credentials provided. Will perform basic validation."
echo "To use your personal developer account, you can run the script with:"
echo " APPLE_ID='your.email@example.com' APPLE_PASSWORD='your-app-specific-password' ./validate-ios.sh"
echo "Note: You need to create an app-specific password at https://appleid.apple.com/account/manage"
fi
# Run validation with detailed output
echo "Running validation with altool..."
if [ -n "$AUTH_ARGS" ]; then
# Use eval to properly handle the quoted arguments
eval "xcrun altool --validate-app -f \"${IPA_PATH}\" --type ios --output-format xml $AUTH_ARGS" 2>&1 | tee "${VALIDATION_OUTPUT}"
else
xcrun altool --validate-app -f "${IPA_PATH}" --type ios --output-format xml 2>&1 | tee "${VALIDATION_OUTPUT}"
fi
VALIDATION_RESULT=$?
# Final validation result
FINAL_VALIDATION_RESULT=0
# Check if validation failed because the app isn't in App Store Connect
if grep -q "No suitable application records were found" "${VALIDATION_OUTPUT}"; then
echo "⚠️ App Store Connect Warning: The app bundle identifier is not found in App Store Connect"
echo "This is expected for apps that haven't been registered in App Store Connect yet."
echo "This doesn't indicate a problem with the build or framework."
# Perform alternative validation
echo "Performing alternative validation checks..."
# Check if IPA was created successfully
if [ -f "${IPA_PATH}" ] && [ -s "${IPA_PATH}" ]; then
echo "✅ IPA file created successfully"
else
echo "❌ IPA file not created or empty"
FINAL_VALIDATION_RESULT=1
fi
# Check if app binary exists and is executable
if [ -f "${TEMP_DIR}/Payload/${APP_NAME}.app/${APP_NAME}" ] && [ -x "${TEMP_DIR}/Payload/${APP_NAME}.app/${APP_NAME}" ]; then
echo "✅ App binary exists and is executable"
else
echo "❌ App binary missing or not executable"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework was properly embedded
if [ -d "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework" ]; then
echo "✅ llama.framework properly embedded"
else
echo "❌ llama.framework not properly embedded"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework binary exists
if [ -f "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework/llama" ]; then
echo "✅ Framework binary exists"
# Further validate framework by checking architecture
ARCHS=$(lipo -info "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework/llama" 2>/dev/null | grep -o "arm64\\|armv7\\|x86_64" | tr '\n' ' ')
if [ -n "$ARCHS" ]; then
echo "✅ Framework architecture(s): $ARCHS"
else
echo "⚠️ Could not determine framework architecture"
fi
else
echo "❌ Framework binary missing"
FINAL_VALIDATION_RESULT=1
fi
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "✅ Alternative validation PASSED: App built successfully with embedded framework"
else
echo "❌ Alternative validation FAILED: Issues found with the app or framework"
fi
elif grep -q "You must specify authentication credentials" "${VALIDATION_OUTPUT}" && [ -z "$AUTH_ARGS" ]; then
echo "✅ iOS Validation PASSED: IPA successfully validated"
echo "Results saved to ${VALIDATION_OUTPUT}"
else
echo "❌ iOS Validation FAILED: IPA validation found issues"
echo "See validation output at ${VALIDATION_OUTPUT}"
echo ""
echo "==== VALIDATION ERRORS ===="
# Try to extract specific errors from the output
if grep -q "Error" "${VALIDATION_OUTPUT}"; then
grep -A 5 "Error" "${VALIDATION_OUTPUT}"
else
# If no specific error found, show the whole log
cat "${VALIDATION_OUTPUT}"
fi
# Additional debugging: check IPA contents
echo ""
echo "==== IPA CONTENTS ===="
mkdir -p "${TEMP_DIR}/ipa_contents"
unzip -q "${IPA_PATH}" -d "${TEMP_DIR}/ipa_contents"
ls -la "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app/"
# Check for code signing issues
echo ""
echo "==== CODE SIGNING INFO ===="
codesign -vv -d "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app" 2>&1 || echo "Code signing verification failed"
# Check embedded frameworks
echo ""
echo "==== FRAMEWORK INFO ===="
ls -la "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
fi
# Don't clean up on error to allow inspection
if [ $FINAL_VALIDATION_RESULT -ne 0 ]; then
echo ""
echo "Temporary files kept for inspection at: ${TEMP_DIR}"
echo "===== iOS Validation Process Failed ====="
exit 1
fi
# Clean up temporary files but keep build artifacts
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "Cleaning up temporary files..."
#rm -rf "${TEMP_DIR}"
fi
echo "===== iOS Validation Process Completed ====="
exit $FINAL_VALIDATION_RESULT
+781
View File
@@ -0,0 +1,781 @@
#!/usr/bin/env bash
# validate-macos.sh - Validate macOS Application with embedded llama.xcframework using SwiftUI
# Authentication options (optional) (can be set via environment variables)
# To use: export APPLE_ID=your.email@example.com
# export APPLE_PASSWORD=your-app-specific-password
# ./validate-macos.sh
APPLE_ID=${APPLE_ID:-""}
APPLE_PASSWORD=${APPLE_PASSWORD:-""}
# Ensure the script exits on error
set -e
# Function to print usage instructions
print_usage() {
echo "Usage: ./validate-macos.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --help Show this help message"
echo " --apple-id EMAIL Apple ID email for validation"
echo " --apple-password PWD App-specific password for Apple ID"
echo ""
echo "Environment variables:"
echo " APPLE_ID Apple ID email for validation"
echo " APPLE_PASSWORD App-specific password for Apple ID"
echo ""
echo "Notes:"
echo " - Command line options take precedence over environment variables"
echo " - Authentication is optional. If not provided, alternative validation will be performed"
echo " - For APPLE_PASSWORD, use an app-specific password generated at https://appleid.apple.com/account/manage"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help)
print_usage
exit 0
;;
--apple-id)
APPLE_ID="$2"
shift 2
;;
--apple-password)
APPLE_PASSWORD="$2"
shift 2
;;
*)
echo "Unknown option: $1"
print_usage
exit 1
;;
esac
done
# Function to clean up in case of error
cleanup() {
# Don't clean up temp files on error to help with debugging
echo "===== macOS Validation Process Failed ====="
exit 1
}
# Set up trap to call cleanup function on error
trap cleanup ERR
set -e # Exit on any error
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
BUILD_DIR="${ROOT_DIR}/validation-builds/ios"
# Configuration
APP_NAME="MacOSLlamaTest"
BUNDLE_ID="org.ggml.MacOSLlamaTest"
XCFRAMEWORK_PATH="${ROOT_DIR}/build-apple/llama.xcframework"
TEMP_DIR="${BUILD_DIR}/temp"
ARCHIVE_PATH="${BUILD_DIR}/${APP_NAME}.xcarchive"
APP_PATH="${BUILD_DIR}/${APP_NAME}.app"
ZIP_PATH="${BUILD_DIR}/${APP_NAME}.zip"
VALIDATION_DIR="${BUILD_DIR}/validation"
# Create necessary directories
mkdir -p "${BUILD_DIR}"
mkdir -p "${TEMP_DIR}"
mkdir -p "${VALIDATION_DIR}"
echo "===== macOS Validation Process Started ====="
# 1. Create a simple test app project
echo "Creating test macOS app project..."
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2025 GGML. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
EOF
# Create SwiftUI app files
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources"
# Create App.swift
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/App.swift" << EOF
import SwiftUI
import llama
@main
struct LlamaTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
EOF
# Create ContentView.swift with macOS specific elements
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/ContentView.swift" << EOF
import SwiftUI
import llama
struct ContentView: View {
// Test that we can initialize a llama context params struct
let params = llama_context_default_params()
var body: some View {
VStack(spacing: 20) {
Text("Llama Framework Test on macOS")
.font(.largeTitle)
.padding()
Text("llama_context_default_params() created successfully")
.font(.headline)
.multilineTextAlignment(.center)
.padding()
// Display some param values to confirm the framework is working
Text("n_ctx: \(params.n_ctx)")
.font(.body)
Text("n_batch: \(params.n_batch)")
.font(.body)
Spacer()
}
.padding()
.frame(width: 600, height: 400)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EOF
# Create project.pbxproj, fixing the framework search paths issues
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
11111111111111111111111 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22222222222222222222222; };
33333333333333333333333 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44444444444444444444444; };
55555555555555555555555 /* llama.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
77777777777777777777777 /* llama.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
88888888888888888888888 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
77777777777777777777777 /* llama.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
99999999999999999999999 /* ${APP_NAME}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "${APP_NAME}.app"; sourceTree = BUILT_PRODUCTS_DIR; };
22222222222222222222222 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
44444444444444444444444 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
66666666666666666666666 /* llama.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = llama.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */
EOF
# Add the rest of the project file with fixed framework search paths
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXFrameworksBuildPhase section */
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
55555555555555555555555 /* llama.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */ = {
isa = PBXGroup;
children = (
99999999999999999999999 /* ${APP_NAME}.app */,
);
name = Products;
sourceTree = "<group>";
};
EOF
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */ = {
isa = PBXGroup;
children = (
66666666666666666666666 /* llama.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
EEEEEEEEEEEEEEEEEEEEEEEE = {
isa = PBXGroup;
children = (
FFFFFFFFFFFFFFFFFFFFFFFF /* MacOSLlamaTest */,
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */,
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */,
);
sourceTree = "<group>";
};
FFFFFFFFFFFFFFFFFFFFFFFF /* MacOSLlamaTest */ = {
isa = PBXGroup;
children = (
1111111111111111111111AA /* Sources */,
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */,
);
path = "MacOSLlamaTest";
sourceTree = "<group>";
};
1111111111111111111111AA /* Sources */ = {
isa = PBXGroup;
children = (
22222222222222222222222 /* App.swift */,
44444444444444444444444 /* ContentView.swift */,
);
path = Sources;
sourceTree = "<group>";
};
/* End PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin PBXNativeTarget section */
3333333333333333333333AA /* ${APP_NAME} */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */;
buildPhases = (
5555555555555555555555AA /* Sources */,
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */,
6666666666666666666666AA /* Resources */,
88888888888888888888888 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "${APP_NAME}";
productName = "${APP_NAME}";
productReference = 99999999999999999999999 /* ${APP_NAME}.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7777777777777777777777AA /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1240;
TargetAttributes = {
3333333333333333333333AA = {
CreatedOnToolsVersion = 12.4;
};
};
};
buildConfigurationList = 8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = EEEEEEEEEEEEEEEEEEEEEEEE;
productRefGroup = CCCCCCCCCCCCCCCCCCCCCCCC /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3333333333333333333333AA /* ${APP_NAME} */,
);
};
/* End PBXProject section */
EOF
# Add the rest of the file with correct FRAMEWORK_SEARCH_PATHS and macOS settings
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXResourcesBuildPhase section */
6666666666666666666666AA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
5555555555555555555555AA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33333333333333333333333 /* ContentView.swift in Sources */,
11111111111111111111111 /* App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
9999999999999999999999AA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
AAAAAAAAAAAAAAAAAAAAABBB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)";
INFOPLIST_FILE = "MacOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.MacOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = "MacOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.MacOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
EOF
# Finish the project.pbxproj file
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin XCConfigurationList section */
8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9999999999999999999999AA /* Debug */,
AAAAAAAAAAAAAAAAAAAAABBB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */,
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7777777777777777777777AA /* Project object */;
}
EOF
# 2. Copy XCFramework to test project
echo "Copying XCFramework to test project..."
cp -R "${XCFRAMEWORK_PATH}" "${TEMP_DIR}/${APP_NAME}/"
# 3. Build and archive the app
echo "Building and archiving test app..."
cd "${TEMP_DIR}/${APP_NAME}"
# Create a simple xcscheme file to avoid xcodebuild scheme issues
mkdir -p "${APP_NAME}.xcodeproj/xcshareddata/xcschemes"
cat > "${APP_NAME}.xcodeproj/xcshareddata/xcschemes/${APP_NAME}.xcscheme" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
EOF
# Now use xcodebuild with an explicitly defined product name for macOS
xcodebuild -project "${APP_NAME}.xcodeproj" -scheme "${APP_NAME}" -sdk macosx -configuration Release archive -archivePath "${ARCHIVE_PATH}" CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO PRODUCT_NAME="${APP_NAME}" SWIFT_OPTIMIZATION_LEVEL="-Onone" -quiet
# 4. Create a package for distribution
echo "Creating distributable package from archive..."
cp -R "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" "${APP_PATH}"
# Check and log app structure
echo "App structure:"
ls -la "${APP_PATH}"
echo "Frameworks:"
ls -la "${APP_PATH}/Contents/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
# Create a zip file for potential distribution
cd "${BUILD_DIR}"
zip -r "${ZIP_PATH}" "${APP_NAME}.app"
# Check embedded provisioning profile
echo "Checking provisioning profile (if any)..."
PROVISIONING_PROFILE=$(find "${APP_PATH}/Contents" -name "embedded.provisionprofile" 2>/dev/null)
if [ -n "$PROVISIONING_PROFILE" ]; then
echo "Found embedded provisioning profile:"
security cms -D -i "$PROVISIONING_PROFILE" || echo "Unable to decode provisioning profile"
else
echo "No embedded provisioning profile found (expected for ad-hoc builds)"
fi
# 5. Validate the app
echo "Validating macOS app..."
VALIDATION_OUTPUT="${VALIDATION_DIR}/validation_output.txt"
# Check if authentication credentials are provided
AUTH_ARGS=""
if [ -n "$APPLE_ID" ] && [ -n "$APPLE_PASSWORD" ]; then
echo "Using Apple ID authentication for validation..."
AUTH_ARGS="--username \"$APPLE_ID\" --password \"$APPLE_PASSWORD\""
else
echo "No authentication credentials provided. Will perform basic validation."
echo "To use your personal developer account, you can run the script with:"
echo " APPLE_ID='your.email@example.com' APPLE_PASSWORD='your-app-specific-password' ./validate-macos.sh"
echo "Note: You need to create an app-specific password at https://appleid.apple.com/account/manage"
fi
# For macOS we need to use notarytool or alternative checks because altool doesn't support macOS apps in the same way
echo "Note: For macOS, formal notarization process would require Apple Developer credentials."
echo "Performing alternative validation checks..."
# Final validation result
FINAL_VALIDATION_RESULT=0
# Check if app was created successfully
if [ -d "${APP_PATH}" ] && [ -s "${APP_PATH}/Contents/MacOS/${APP_NAME}" ]; then
echo "✅ App package created successfully"
else
echo "❌ App package not created or binary missing"
FINAL_VALIDATION_RESULT=1
fi
# Check if app binary exists and is executable
if [ -f "${APP_PATH}/Contents/MacOS/${APP_NAME}" ] && [ -x "${APP_PATH}/Contents/MacOS/${APP_NAME}" ]; then
echo "✅ App binary exists and is executable"
else
echo "❌ App binary missing or not executable"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework was properly embedded
if [ -d "${APP_PATH}/Contents/Frameworks/llama.framework" ]; then
echo "✅ llama.framework properly embedded"
else
echo "❌ llama.framework not properly embedded"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework binary exists
if [ -f "${APP_PATH}/Contents/Frameworks/llama.framework/Versions/A/llama" ]; then
echo "✅ Framework binary exists"
# Further validate framework by checking architecture
ARCHS=$(lipo -info "${APP_PATH}/Contents/Frameworks/llama.framework/Versions/A/llama" 2>/dev/null | grep -o "arm64\\|x86_64" | tr '\n' ' ')
if [ -n "$ARCHS" ]; then
echo "✅ Framework architecture(s): $ARCHS"
else
echo "⚠️ Could not determine framework architecture"
fi
else
echo "❌ Framework binary missing"
FINAL_VALIDATION_RESULT=1
fi
# Check code signing
echo ""
echo "==== CODE SIGNING INFO ===="
codesign -vv -d "${APP_PATH}" 2>&1 || echo "Code signing verification not available (expected for ad-hoc builds)"
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
if [ -n "$AUTH_ARGS" ]; then
echo ""
echo "To notarize this app with Apple (requires Apple Developer account):"
echo "xcrun notarytool submit \"${ZIP_PATH}\" --apple-id \"your-apple-id\" --password \"your-app-specific-password\" --team-id \"your-team-id\" --wait"
echo ""
fi
echo "✅ Validation PASSED: macOS app built successfully with embedded framework"
else
echo "❌ Validation FAILED: Issues found with the app or framework"
fi
# Don't clean up on error to allow inspection
if [ $FINAL_VALIDATION_RESULT -ne 0 ]; then
echo ""
echo "Temporary files kept for inspection at: ${TEMP_DIR}"
echo "===== macOS Validation Process Failed ====="
exit 1
fi
# Clean up temporary files but keep build artifacts
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "Cleaning up temporary files..."
#rm -rf "${TEMP_DIR}"
fi
echo "===== macOS Validation Process Completed ====="
echo "App package available at: ${APP_PATH}"
echo "Zipped app available at: ${ZIP_PATH}"
exit $FINAL_VALIDATION_RESULT
+813
View File
@@ -0,0 +1,813 @@
#!/usr/bin/env bash
# validate-tvos.sh - Validate tvOS Application with embedded llama.xcframework using SwiftUI
# Authentication options (optional) (can be set via environment variables)
# To use: export APPLE_ID=your.email@example.com
# export APPLE_PASSWORD=your-app-specific-password
# ./validate-tvos.sh
APPLE_ID=${APPLE_ID:-""}
APPLE_PASSWORD=${APPLE_PASSWORD:-""}
# Ensure the script exits on error
set -e
# Function to print usage instructions
print_usage() {
echo "Usage: ./validate-tvos.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --help Show this help message"
echo " --apple-id EMAIL Apple ID email for validation"
echo " --apple-password PWD App-specific password for Apple ID"
echo ""
echo "Environment variables:"
echo " APPLE_ID Apple ID email for validation"
echo " APPLE_PASSWORD App-specific password for Apple ID"
echo ""
echo "Notes:"
echo " - Command line options take precedence over environment variables"
echo " - Authentication is optional. If not provided, alternative validation will be performed"
echo " - For APPLE_PASSWORD, use an app-specific password generated at https://appleid.apple.com/account/manage"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help)
print_usage
exit 0
;;
--apple-id)
APPLE_ID="$2"
shift 2
;;
--apple-password)
APPLE_PASSWORD="$2"
shift 2
;;
*)
echo "Unknown option: $1"
print_usage
exit 1
;;
esac
done
# Function to clean up in case of error
cleanup() {
# Don't clean up temp files on error to help with debugging
echo "===== tvOS Validation Process Failed ====="
exit 1
}
# Set up trap to call cleanup function on error
trap cleanup ERR
set -e # Exit on any error
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
BUILD_DIR="${ROOT_DIR}/validation-builds/ios"
# Configuration
APP_NAME="TVOSLlamaTest"
BUNDLE_ID="org.ggml.TVOSLlamaTest"
XCFRAMEWORK_PATH="${ROOT_DIR}/build-apple/llama.xcframework"
TEMP_DIR="${BUILD_DIR}/temp"
ARCHIVE_PATH="${BUILD_DIR}/${APP_NAME}.xcarchive"
IPA_PATH="${BUILD_DIR}/${APP_NAME}.ipa"
VALIDATION_DIR="${BUILD_DIR}/validation"
# Create necessary directories
mkdir -p "${BUILD_DIR}"
mkdir -p "${TEMP_DIR}"
mkdir -p "${VALIDATION_DIR}"
echo "===== tvOS Validation Process Started ====="
# 1. Create a simple test app project
echo "Creating test tvOS app project..."
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
</dict>
</plist>
EOF
# Create SwiftUI app files
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources"
# Create App.swift
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/App.swift" << EOF
import SwiftUI
import llama
@main
struct LlamaTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
EOF
# Create ContentView.swift with tvOS specific elements
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/ContentView.swift" << EOF
import SwiftUI
import llama
struct ContentView: View {
// Test that we can initialize a llama context params struct
let params = llama_context_default_params()
var body: some View {
VStack(spacing: 40) {
Text("Llama Framework Test on tvOS")
.font(.largeTitle)
.padding()
Text("llama_context_default_params() created successfully")
.font(.headline)
.multilineTextAlignment(.center)
.padding()
// Display some param values to confirm the framework is working
Text("n_ctx: \(params.n_ctx)")
.font(.title2)
Text("n_batch: \(params.n_batch)")
.font(.title2)
Spacer()
}
.padding(50)
// Larger size suitable for TV display
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EOF
# Create project.pbxproj, fixing the framework search paths issues
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
11111111111111111111111 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22222222222222222222222; };
33333333333333333333333 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44444444444444444444444; };
55555555555555555555555 /* llama.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
77777777777777777777777 /* llama.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
88888888888888888888888 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
77777777777777777777777 /* llama.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
99999999999999999999999 /* ${APP_NAME}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "${APP_NAME}.app"; sourceTree = BUILT_PRODUCTS_DIR; };
22222222222222222222222 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
44444444444444444444444 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
66666666666666666666666 /* llama.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = llama.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */
EOF
# Add the rest of the project file with fixed framework search paths
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXFrameworksBuildPhase section */
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
55555555555555555555555 /* llama.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */ = {
isa = PBXGroup;
children = (
99999999999999999999999 /* ${APP_NAME}.app */,
);
name = Products;
sourceTree = "<group>";
};
EOF
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */ = {
isa = PBXGroup;
children = (
66666666666666666666666 /* llama.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
EEEEEEEEEEEEEEEEEEEEEEEE = {
isa = PBXGroup;
children = (
FFFFFFFFFFFFFFFFFFFFFFFF /* TVOSLlamaTest */,
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */,
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */,
);
sourceTree = "<group>";
};
FFFFFFFFFFFFFFFFFFFFFFFF /* TVOSLlamaTest */ = {
isa = PBXGroup;
children = (
1111111111111111111111AA /* Sources */,
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */,
);
path = "TVOSLlamaTest";
sourceTree = "<group>";
};
1111111111111111111111AA /* Sources */ = {
isa = PBXGroup;
children = (
22222222222222222222222 /* App.swift */,
44444444444444444444444 /* ContentView.swift */,
);
path = Sources;
sourceTree = "<group>";
};
/* End PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin PBXNativeTarget section */
3333333333333333333333AA /* ${APP_NAME} */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */;
buildPhases = (
5555555555555555555555AA /* Sources */,
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */,
6666666666666666666666AA /* Resources */,
88888888888888888888888 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "${APP_NAME}";
productName = "${APP_NAME}";
productReference = 99999999999999999999999 /* ${APP_NAME}.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7777777777777777777777AA /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1240;
TargetAttributes = {
3333333333333333333333AA = {
CreatedOnToolsVersion = 12.4;
};
};
};
buildConfigurationList = 8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = EEEEEEEEEEEEEEEEEEEEEEEE;
productRefGroup = CCCCCCCCCCCCCCCCCCCCCCCC /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3333333333333333333333AA /* ${APP_NAME} */,
);
};
/* End PBXProject section */
EOF
# Add the rest of the file with correct FRAMEWORK_SEARCH_PATHS and tvOS settings
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXResourcesBuildPhase section */
6666666666666666666666AA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
5555555555555555555555AA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33333333333333333333333 /* ContentView.swift in Sources */,
11111111111111111111111 /* App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
9999999999999999999999AA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
TVOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = appletvos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
AAAAAAAAAAAAAAAAAAAAABBB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
TVOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = appletvos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)";
INFOPLIST_FILE = "TVOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.TVOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
};
name = Debug;
};
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = "TVOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.TVOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
};
name = Release;
};
/* End XCBuildConfiguration section */
EOF
# Finish the project.pbxproj file
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin XCConfigurationList section */
8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9999999999999999999999AA /* Debug */,
AAAAAAAAAAAAAAAAAAAAABBB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */,
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7777777777777777777777AA /* Project object */;
}
EOF
# 2. Copy XCFramework to test project
echo "Copying XCFramework to test project..."
cp -R "${XCFRAMEWORK_PATH}" "${TEMP_DIR}/${APP_NAME}/"
# 3. Build and archive the app
echo "Building and archiving test app..."
cd "${TEMP_DIR}/${APP_NAME}"
# Create a simple xcscheme file to avoid xcodebuild scheme issues
mkdir -p "${APP_NAME}.xcodeproj/xcshareddata/xcschemes"
cat > "${APP_NAME}.xcodeproj/xcshareddata/xcschemes/${APP_NAME}.xcscheme" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
EOF
# Now use xcodebuild with an explicitly defined product name for tvOS
xcodebuild -project "${APP_NAME}.xcodeproj" -scheme "${APP_NAME}" -sdk appletvos -configuration Release archive -archivePath "${ARCHIVE_PATH}" CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO PRODUCT_NAME="${APP_NAME}" SWIFT_OPTIMIZATION_LEVEL="-Onone" -quiet
# 4. Create IPA from archive
echo "Creating IPA from archive..."
mkdir -p "${TEMP_DIR}/Payload"
cp -R "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" "${TEMP_DIR}/Payload/"
# Check and log app structure before zipping
echo "App structure:"
ls -la "${TEMP_DIR}/Payload/${APP_NAME}.app/"
echo "Frameworks:"
ls -la "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
cd "${TEMP_DIR}"
zip -r "${IPA_PATH}" Payload
# Check embedded provisioning profile
echo "Checking provisioning profile (if any)..."
PROVISIONING_PROFILE=$(find "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" -name "embedded.mobileprovision" 2>/dev/null)
if [ -n "$PROVISIONING_PROFILE" ]; then
echo "Found embedded provisioning profile:"
security cms -D -i "$PROVISIONING_PROFILE" || echo "Unable to decode provisioning profile"
else
echo "No embedded provisioning profile found (expected for ad-hoc builds)"
fi
# 5. Validate the IPA
echo "Validating IPA..."
VALIDATION_OUTPUT="${VALIDATION_DIR}/validation_output.txt"
# Check if authentication credentials are provided
AUTH_ARGS=""
if [ -n "$APPLE_ID" ] && [ -n "$APPLE_PASSWORD" ]; then
echo "Using Apple ID authentication for validation..."
AUTH_ARGS="--username \"$APPLE_ID\" --password \"$APPLE_PASSWORD\""
else
echo "No authentication credentials provided. Will perform basic validation."
echo "To use your personal developer account, you can run the script with:"
echo " APPLE_ID='your.email@example.com' APPLE_PASSWORD='your-app-specific-password' ./validate-tvos.sh"
echo "Note: You need to create an app-specific password at https://appleid.apple.com/account/manage"
fi
# Run validation with detailed output
echo "Running validation with altool..."
if [ -n "$AUTH_ARGS" ]; then
# Use eval to properly handle the quoted arguments
eval "xcrun altool --validate-app -f \"${IPA_PATH}\" --type tvos --output-format xml $AUTH_ARGS" 2>&1 | tee "${VALIDATION_OUTPUT}"
else
xcrun altool --validate-app -f "${IPA_PATH}" --type tvos --output-format xml 2>&1 | tee "${VALIDATION_OUTPUT}"
fi
VALIDATION_RESULT=$?
# Final validation result
FINAL_VALIDATION_RESULT=0
# Check if validation failed because the app isn't in App Store Connect
if grep -q "No suitable application records were found" "${VALIDATION_OUTPUT}"; then
echo "⚠️ App Store Connect Warning: The app bundle identifier is not found in App Store Connect"
echo "This is expected for apps that haven't been registered in App Store Connect yet."
echo "This doesn't indicate a problem with the build or framework."
# Perform alternative validation
echo "Performing alternative validation checks..."
# Check if IPA was created successfully
if [ -f "${IPA_PATH}" ] && [ -s "${IPA_PATH}" ]; then
echo "✅ IPA file created successfully"
else
echo "❌ IPA file not created or empty"
FINAL_VALIDATION_RESULT=1
fi
# Check if app binary exists and is executable
if [ -f "${TEMP_DIR}/Payload/${APP_NAME}.app/${APP_NAME}" ] && [ -x "${TEMP_DIR}/Payload/${APP_NAME}.app/${APP_NAME}" ]; then
echo "✅ App binary exists and is executable"
else
echo "❌ App binary missing or not executable"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework was properly embedded
if [ -d "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework" ]; then
echo "✅ llama.framework properly embedded"
else
echo "❌ llama.framework not properly embedded"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework binary exists
if [ -f "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework/llama" ]; then
echo "✅ Framework binary exists"
# Further validate framework by checking architecture
ARCHS=$(lipo -info "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework/llama" 2>/dev/null | grep -o "arm64\\|x86_64" | tr '\n' ' ')
if [ -n "$ARCHS" ]; then
echo "✅ Framework architecture(s): $ARCHS"
else
echo "⚠️ Could not determine framework architecture"
fi
else
echo "❌ Framework binary missing"
FINAL_VALIDATION_RESULT=1
fi
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "✅ Alternative validation PASSED: App built successfully with embedded framework"
else
echo "❌ Alternative validation FAILED: Issues found with the app or framework"
fi
elif grep -q "You must specify authentication credentials" "${VALIDATION_OUTPUT}" && [ -z "$AUTH_ARGS" ]; then
echo "✅ tvOS Validation PASSED: IPA successfully validated"
echo "Results saved to ${VALIDATION_OUTPUT}"
else
echo "❌ tvOS Validation FAILED: IPA validation found issues"
echo "See validation output at ${VALIDATION_OUTPUT}"
echo ""
echo "==== VALIDATION ERRORS ===="
# Try to extract specific errors from the output
if grep -q "Error" "${VALIDATION_OUTPUT}"; then
grep -A 5 "Error" "${VALIDATION_OUTPUT}"
else
# If no specific error found, show the whole log
cat "${VALIDATION_OUTPUT}"
fi
# Additional debugging: check IPA contents
echo ""
echo "==== IPA CONTENTS ===="
mkdir -p "${TEMP_DIR}/ipa_contents"
unzip -q "${IPA_PATH}" -d "${TEMP_DIR}/ipa_contents"
ls -la "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app/"
# Check for code signing issues
echo ""
echo "==== CODE SIGNING INFO ===="
codesign -vv -d "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app" 2>&1 || echo "Code signing verification failed"
# Check embedded frameworks
echo ""
echo "==== FRAMEWORK INFO ===="
ls -la "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
fi
# Don't clean up on error to allow inspection
if [ $FINAL_VALIDATION_RESULT -ne 0 ]; then
echo ""
echo "Temporary files kept for inspection at: ${TEMP_DIR}"
echo "===== tvOS Validation Process Failed ====="
exit 1
fi
# Clean up temporary files but keep build artifacts
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "Cleaning up temporary files..."
#rm -rf "${TEMP_DIR}"
fi
echo "===== tvOS Validation Process Completed ====="
exit $FINAL_VALIDATION_RESULT
+811
View File
@@ -0,0 +1,811 @@
#!/usr/bin/env bash
# validate-visionos.sh - Validate visionOS Application with embedded llama.xcframework using SwiftUI
# Authentication options (optional) (can be set via environment variables)
# To use: export APPLE_ID=your.email@example.com
# export APPLE_PASSWORD=your-app-specific-password
# ./validate-visionos.sh
APPLE_ID=${APPLE_ID:-""}
APPLE_PASSWORD=${APPLE_PASSWORD:-""}
# Ensure the script exits on error
set -e
# Function to print usage instructions
print_usage() {
echo "Usage: ./validate-visionos.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --help Show this help message"
echo " --apple-id EMAIL Apple ID email for validation"
echo " --apple-password PWD App-specific password for Apple ID"
echo ""
echo "Environment variables:"
echo " APPLE_ID Apple ID email for validation"
echo " APPLE_PASSWORD App-specific password for Apple ID"
echo ""
echo "Notes:"
echo " - Command line options take precedence over environment variables"
echo " - Authentication is optional. If not provided, alternative validation will be performed"
echo " - For APPLE_PASSWORD, use an app-specific password generated at https://appleid.apple.com/account/manage"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help)
print_usage
exit 0
;;
--apple-id)
APPLE_ID="$2"
shift 2
;;
--apple-password)
APPLE_PASSWORD="$2"
shift 2
;;
*)
echo "Unknown option: $1"
print_usage
exit 1
;;
esac
done
# Function to clean up in case of error
cleanup() {
# Don't clean up temp files on error to help with debugging
echo "===== visionOS Validation Process Failed ====="
exit 1
}
# Set up trap to call cleanup function on error
trap cleanup ERR
set -e # Exit on any error
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
BUILD_DIR="${ROOT_DIR}/validation-builds/visionos"
# Configuration
APP_NAME="VisionOSLlamaTest"
BUNDLE_ID="org.ggml.VisionOSLlamaTest"
XCFRAMEWORK_PATH="${ROOT_DIR}/build-apple/llama.xcframework"
TEMP_DIR="${BUILD_DIR}/temp"
ARCHIVE_PATH="${BUILD_DIR}/${APP_NAME}.xcarchive"
IPA_PATH="${BUILD_DIR}/${APP_NAME}.ipa"
VALIDATION_DIR="${BUILD_DIR}/validation"
# Create necessary directories
mkdir -p "${BUILD_DIR}"
mkdir -p "${TEMP_DIR}"
mkdir -p "${VALIDATION_DIR}"
echo "===== visionOS Validation Process Started ====="
# 1. Create a simple test app project
echo "Creating test visionOS app project..."
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
EOF
# Create SwiftUI app files
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources"
# Create App.swift
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/App.swift" << EOF
import SwiftUI
import llama
@main
struct LlamaTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
EOF
# Create ContentView.swift with visionOS specific elements
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}/Sources/ContentView.swift" << EOF
import SwiftUI
import llama
struct ContentView: View {
// Test that we can initialize a llama context params struct
let params = llama_context_default_params()
var body: some View {
VStack(spacing: 20) {
Text("Llama Framework Test on visionOS")
.font(.largeTitle)
.padding()
Text("llama_context_default_params() created successfully")
.font(.headline)
.multilineTextAlignment(.center)
.padding()
// Display some param values to confirm the framework is working
Text("n_ctx: \(params.n_ctx)")
.font(.body)
Text("n_batch: \(params.n_batch)")
.font(.body)
Spacer()
}
.padding()
.frame(width: 500, height: 400)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EOF
# Create project.pbxproj, fixing the framework search paths issues
mkdir -p "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj"
cat > "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
11111111111111111111111 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22222222222222222222222; };
33333333333333333333333 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44444444444444444444444; };
55555555555555555555555 /* llama.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
77777777777777777777777 /* llama.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 66666666666666666666666; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
88888888888888888888888 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
77777777777777777777777 /* llama.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
99999999999999999999999 /* ${APP_NAME}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "${APP_NAME}.app"; sourceTree = BUILT_PRODUCTS_DIR; };
22222222222222222222222 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
44444444444444444444444 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
66666666666666666666666 /* llama.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = llama.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */
EOF
# Add the rest of the project file with fixed framework search paths
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXFrameworksBuildPhase section */
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
55555555555555555555555 /* llama.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */ = {
isa = PBXGroup;
children = (
99999999999999999999999 /* ${APP_NAME}.app */,
);
name = Products;
sourceTree = "<group>";
};
EOF
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */ = {
isa = PBXGroup;
children = (
66666666666666666666666 /* llama.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
EEEEEEEEEEEEEEEEEEEEEEEE = {
isa = PBXGroup;
children = (
FFFFFFFFFFFFFFFFFFFFFFFF /* VisionOSLlamaTest */,
CCCCCCCCCCCCCCCCCCCCCCCC /* Products */,
DDDDDDDDDDDDDDDDDDDDDDDD /* Frameworks */,
);
sourceTree = "<group>";
};
FFFFFFFFFFFFFFFFFFFFFFFF /* VisionOSLlamaTest */ = {
isa = PBXGroup;
children = (
1111111111111111111111AA /* Sources */,
AAAAAAAAAAAAAAAAAAAAAAA /* Info.plist */,
);
path = "VisionOSLlamaTest";
sourceTree = "<group>";
};
1111111111111111111111AA /* Sources */ = {
isa = PBXGroup;
children = (
22222222222222222222222 /* App.swift */,
44444444444444444444444 /* ContentView.swift */,
);
path = Sources;
sourceTree = "<group>";
};
/* End PBXGroup section */
EOF
# Continue with the project.pbxproj file, using the APP_NAME variable appropriately
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin PBXNativeTarget section */
3333333333333333333333AA /* ${APP_NAME} */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */;
buildPhases = (
5555555555555555555555AA /* Sources */,
BBBBBBBBBBBBBBBBBBBBBBBB /* Frameworks */,
6666666666666666666666AA /* Resources */,
88888888888888888888888 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "${APP_NAME}";
productName = "${APP_NAME}";
productReference = 99999999999999999999999 /* ${APP_NAME}.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7777777777777777777777AA /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1510;
LastUpgradeCheck = 1510;
TargetAttributes = {
3333333333333333333333AA = {
CreatedOnToolsVersion = 15.1;
};
};
};
buildConfigurationList = 8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */;
compatibilityVersion = "Xcode 15.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = EEEEEEEEEEEEEEEEEEEEEEEE;
productRefGroup = CCCCCCCCCCCCCCCCCCCCCCCC /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3333333333333333333333AA /* ${APP_NAME} */,
);
};
/* End PBXProject section */
EOF
# Add the rest of the file with correct FRAMEWORK_SEARCH_PATHS
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << 'EOF'
/* Begin PBXResourcesBuildPhase section */
6666666666666666666666AA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
5555555555555555555555AA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33333333333333333333333 /* ContentView.swift in Sources */,
11111111111111111111111 /* App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
9999999999999999999999AA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = xros;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Debug;
};
AAAAAAAAAAAAAAAAAAAAABBB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = xros;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
XROS_DEPLOYMENT_TARGET = 1.0;
};
name = Release;
};
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)";
INFOPLIST_FILE = "VisionOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.VisionOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "xros xrsimulator";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
};
name = Debug;
};
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = "VisionOSLlamaTest/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.ggml.VisionOSLlamaTest";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "xros xrsimulator";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
};
name = Release;
};
/* End XCBuildConfiguration section */
EOF
# Finish the project.pbxproj file
cat >> "${TEMP_DIR}/${APP_NAME}/${APP_NAME}.xcodeproj/project.pbxproj" << EOF
/* Begin XCConfigurationList section */
8888888888888888888888AA /* Build configuration list for PBXProject "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9999999999999999999999AA /* Debug */,
AAAAAAAAAAAAAAAAAAAAABBB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4444444444444444444444AA /* Build configuration list for PBXNativeTarget "${APP_NAME}" */ = {
isa = XCConfigurationList;
buildConfigurations = (
BBBBBBBBBBBBBBBBBBBBBBCCC /* Debug */,
CCCCCCCCCCCCCCCCCCCCCCDDD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7777777777777777777777AA /* Project object */;
}
EOF
# 2. Copy XCFramework to test project
echo "Copying XCFramework to test project..."
cp -R "${XCFRAMEWORK_PATH}" "${TEMP_DIR}/${APP_NAME}/"
# 3. Build and archive the app
echo "Building and archiving test app..."
cd "${TEMP_DIR}/${APP_NAME}"
# Create a simple xcscheme file to avoid xcodebuild scheme issues
mkdir -p "${APP_NAME}.xcodeproj/xcshareddata/xcschemes"
cat > "${APP_NAME}.xcodeproj/xcshareddata/xcschemes/${APP_NAME}.xcscheme" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3333333333333333333333AA"
BuildableName = "${APP_NAME}.app"
BlueprintName = "${APP_NAME}"
ReferencedContainer = "container:${APP_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
EOF
# Now use xcodebuild with an explicitly defined product name for visionOS
xcodebuild -project "${APP_NAME}.xcodeproj" -scheme "${APP_NAME}" -sdk xros -configuration Release archive -archivePath "${ARCHIVE_PATH}" CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO PRODUCT_NAME="${APP_NAME}" SWIFT_OPTIMIZATION_LEVEL="-Onone" -quiet
# 4. Create IPA from archive
echo "Creating IPA from archive..."
mkdir -p "${TEMP_DIR}/Payload"
cp -R "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" "${TEMP_DIR}/Payload/"
# Check and log app structure before zipping
echo "App structure:"
ls -la "${TEMP_DIR}/Payload/${APP_NAME}.app/"
echo "Frameworks:"
ls -la "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
cd "${TEMP_DIR}"
zip -r "${IPA_PATH}" Payload
# Check embedded provisioning profile
echo "Checking provisioning profile (if any)..."
PROVISIONING_PROFILE=$(find "${ARCHIVE_PATH}/Products/Applications/${APP_NAME}.app" -name "embedded.mobileprovision" 2>/dev/null)
if [ -n "$PROVISIONING_PROFILE" ]; then
echo "Found embedded provisioning profile:"
security cms -D -i "$PROVISIONING_PROFILE" || echo "Unable to decode provisioning profile"
else
echo "No embedded provisioning profile found (expected for ad-hoc builds)"
fi
# 5. Validate the IPA
echo "Validating IPA..."
VALIDATION_OUTPUT="${VALIDATION_DIR}/validation_output.txt"
# Check if authentication credentials are provided
AUTH_ARGS=""
if [ -n "$APPLE_ID" ] && [ -n "$APPLE_PASSWORD" ]; then
echo "Using Apple ID authentication for validation..."
AUTH_ARGS="--username \"$APPLE_ID\" --password \"$APPLE_PASSWORD\""
else
echo "No authentication credentials provided. Will perform basic validation."
echo "To use your personal developer account, you can run the script with:"
echo " APPLE_ID='your.email@example.com' APPLE_PASSWORD='your-app-specific-password' ./validate-visionos.sh"
echo "Note: You need to create an app-specific password at https://appleid.apple.com/account/manage"
fi
# Run validation with detailed output
echo "Running validation with altool..."
if [ -n "$AUTH_ARGS" ]; then
# Use eval to properly handle the quoted arguments
eval "xcrun altool --validate-app -f \"${IPA_PATH}\" --type visionos --output-format xml $AUTH_ARGS" 2>&1 | tee "${VALIDATION_OUTPUT}"
else
xcrun altool --validate-app -f "${IPA_PATH}" --type visionos --output-format xml 2>&1 | tee "${VALIDATION_OUTPUT}"
fi
VALIDATION_RESULT=$?
# Final validation result
FINAL_VALIDATION_RESULT=0
# Check if validation failed because the app isn't in App Store Connect
if grep -q "No suitable application records were found" "${VALIDATION_OUTPUT}"; then
echo "⚠️ App Store Connect Warning: The app bundle identifier is not found in App Store Connect"
echo "This is expected for apps that haven't been registered in App Store Connect yet."
echo "This doesn't indicate a problem with the build or framework."
# Perform alternative validation
echo "Performing alternative validation checks..."
# Check if IPA was created successfully
if [ -f "${IPA_PATH}" ] && [ -s "${IPA_PATH}" ]; then
echo "✅ IPA file created successfully"
else
echo "❌ IPA file not created or empty"
FINAL_VALIDATION_RESULT=1
fi
# Check if app binary exists and is executable
if [ -f "${TEMP_DIR}/Payload/${APP_NAME}.app/${APP_NAME}" ] && [ -x "${TEMP_DIR}/Payload/${APP_NAME}.app/${APP_NAME}" ]; then
echo "✅ App binary exists and is executable"
else
echo "❌ App binary missing or not executable"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework was properly embedded
if [ -d "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework" ]; then
echo "✅ llama.framework properly embedded"
else
echo "❌ llama.framework not properly embedded"
FINAL_VALIDATION_RESULT=1
fi
# Check if framework binary exists
if [ -f "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework/llama" ]; then
echo "✅ Framework binary exists"
# Further validate framework by checking architecture
ARCHS=$(lipo -info "${TEMP_DIR}/Payload/${APP_NAME}.app/Frameworks/llama.framework/llama" 2>/dev/null | grep -o "arm64\\|x86_64" | tr '\n' ' ')
if [ -n "$ARCHS" ]; then
echo "✅ Framework architecture(s): $ARCHS"
else
echo "⚠️ Could not determine framework architecture"
fi
else
echo "❌ Framework binary missing"
FINAL_VALIDATION_RESULT=1
fi
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "✅ Alternative validation PASSED: App built successfully with embedded framework"
else
echo "❌ Alternative validation FAILED: Issues found with the app or framework"
fi
elif grep -q "You must specify authentication credentials" "${VALIDATION_OUTPUT}" && [ -z "$AUTH_ARGS" ]; then
echo "✅ visionOS Validation PASSED: IPA successfully validated"
echo "Results saved to ${VALIDATION_OUTPUT}"
else
echo "❌ visionOS Validation FAILED: IPA validation found issues"
echo "See validation output at ${VALIDATION_OUTPUT}"
echo ""
echo "==== VALIDATION ERRORS ===="
# Try to extract specific errors from the output
if grep -q "Error" "${VALIDATION_OUTPUT}"; then
grep -A 5 "Error" "${VALIDATION_OUTPUT}"
else
# If no specific error found, show the whole log
cat "${VALIDATION_OUTPUT}"
fi
# Additional debugging: check IPA contents
echo ""
echo "==== IPA CONTENTS ===="
mkdir -p "${TEMP_DIR}/ipa_contents"
unzip -q "${IPA_PATH}" -d "${TEMP_DIR}/ipa_contents"
ls -la "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app/"
# Check for code signing issues
echo ""
echo "==== CODE SIGNING INFO ===="
codesign -vv -d "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app" 2>&1 || echo "Code signing verification failed"
# Check embedded frameworks
echo ""
echo "==== FRAMEWORK INFO ===="
ls -la "${TEMP_DIR}/ipa_contents/Payload/${APP_NAME}.app/Frameworks/" 2>/dev/null || echo "No Frameworks directory found"
fi
# Don't clean up on error to allow inspection
if [ $FINAL_VALIDATION_RESULT -ne 0 ]; then
echo ""
echo "Temporary files kept for inspection at: ${TEMP_DIR}"
echo "===== visionOS Validation Process Failed ====="
exit 1
fi
# Clean up temporary files but keep build artifacts
if [ $FINAL_VALIDATION_RESULT -eq 0 ]; then
echo "Cleaning up temporary files..."
#rm -rf "${TEMP_DIR}"
fi
echo "===== visionOS Validation Process Completed ====="
exit $FINAL_VALIDATION_RESULT
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
RESULTS="bench-models-results.txt"
: > "$RESULTS"
ARGS_BB="-c 270336 -npp 512,4096,8192 -npl 1,2,4,8,16,32 -ntg 32"
ARGS_B="-d 0,4096,8192,16384,32768 -p 2048 -n 32"
QUICK=0
DIO=0
while (( "$#" )); do
case "$1" in
--quick) QUICK=1; shift ;;
--dio) DIO=1; shift ;;
*) shift ;;
esac
done
if (( QUICK )); then
ARGS_BB="-c 20480 -npp 512,4096 -npl 1,2,4 -ntg 32"
ARGS_B="-d 0 -p 2048 -n 32"
fi
if (( DIO )); then
ARGS_BB="${ARGS_BB} --load-mode dio"
ARGS_B="${ARGS_B} --load-mode dio"
fi
run_model() {
local HFR=$1
local HFF=$2
printf "## ${HFR}\n" | tee -a "$RESULTS"
printf "\n" | tee -a "$RESULTS"
printf "Model: https://huggingface.co/${HFR}\n" | tee -a "$RESULTS"
printf "\n" | tee -a "$RESULTS"
printf -- "- \`llama-batched-bench\`\n" | tee -a "$RESULTS"
printf "\n" | tee -a "$RESULTS"
./bin/llama-batched-bench \
-hfr "${HFR}" -hff "${HFF}" \
-m "${HFF}" -fa 1 -ub 2048 \
${ARGS_BB} | tee -a "$RESULTS"
printf "\n" | tee -a "$RESULTS"
printf -- "- \`llama-bench\`\n" | tee -a "$RESULTS"
printf "\n" | tee -a "$RESULTS"
./bin/llama-bench \
-m "${HFF}" -fa 1 -ub 2048 \
${ARGS_B} | tee -a "$RESULTS"
printf "\n" | tee -a "$RESULTS"
printf "\n"
}
run_model "ggml-org/gpt-oss-20b-GGUF" "gpt-oss-20b-mxfp4.gguf"
run_model "ggml-org/gpt-oss-120b-GGUF" "gpt-oss-120b-mxfp4-00001-of-00003.gguf"
run_model "ggml-org/Qwen3-Coder-30B-A3B-Instruct-Q8_0-GGUF" "qwen3-coder-30b-a3b-instruct-q8_0.gguf"
run_model "ggml-org/Qwen2.5-Coder-7B-Q8_0-GGUF" "qwen2.5-coder-7b-q8_0.gguf"
run_model "ggml-org/gemma-3-4b-it-qat-GGUF" "gemma-3-4b-it-qat-Q4_0.gguf"
run_model "ggml-org/GLM-4.7-Flash-GGUF" "GLM-4.7-Flash-Q8_0.gguf"
if [[ -f models-extra.txt ]]; then
while read -r HFR HFF; do
[[ -z "$HFR" ]] && continue
run_model "$HFR" "$HFF"
done < models-extra.txt
fi
printf "\n=====================================\n"
printf "\n"
cat "$RESULTS"
printf "\n"
printf "Done! Results are written to $RESULTS\n"
printf "\n"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
CC=$1
build_number="0"
build_commit="unknown"
build_compiler="unknown"
build_target="unknown"
if out=$(git rev-list --count HEAD); then
# git is broken on WSL so we need to strip extra newlines
build_number=$(printf '%s' "$out" | tr -d '\n')
fi
if out=$(git rev-parse --short HEAD); then
build_commit=$(printf '%s' "$out" | tr -d '\n')
fi
if out=$($CC --version | head -1); then
build_compiler=$out
fi
if out=$($CC -dumpmachine); then
build_target=$out
fi
echo "int LLAMA_BUILD_NUMBER = ${build_number};"
echo "char const *LLAMA_COMMIT = \"${build_commit}\";"
echo "char const *LLAMA_COMPILER = \"${build_compiler}\";"
echo "char const *LLAMA_BUILD_TARGET = \"${build_target}\";"
+136
View File
@@ -0,0 +1,136 @@
# Compile-time profiling using clang -ftime-trace + ClangBuildAnalyzer.
#
# Usage:
# .\scripts\build-profile.ps1 [-Full] [-Jobs N]
#
# -Full : include Server, Tools, and Tests (default: minimal build)
# -Jobs : number of parallel jobs (default: all cores)
#
# Requires ClangBuildAnalyzer:
# https://github.com/aras-p/ClangBuildAnalyzer
param(
[switch]$Full,
[int]$Jobs = [Environment]::ProcessorCount
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RootDir = Split-Path -Parent $ScriptDir
if ($Full) {
$BuildDir = Join-Path $RootDir "build-profile-full"
$Report = Join-Path $BuildDir "profile-report-full.txt"
} else {
$BuildDir = Join-Path $RootDir "build-profile-baseline"
$Report = Join-Path $BuildDir "profile-report.txt"
}
$OutputBin = Join-Path $BuildDir "clang_analysis.bin"
if (-not (Get-Command clang++ -ErrorAction SilentlyContinue)) {
Write-Error "clang++ not found"
exit 1
}
if (-not (Get-Command ninja -ErrorAction SilentlyContinue)) {
Write-Error "ninja not found (required so cmake does not fall back to the Visual Studio/MSVC generator)"
exit 1
}
if (-not (Get-Command ClangBuildAnalyzer -ErrorAction SilentlyContinue)) {
Write-Error "ClangBuildAnalyzer not found`n https://github.com/aras-p/ClangBuildAnalyzer/releases"
exit 1
}
$ClangVer = (clang++ --version | Select-Object -First 1)
Write-Host "compiler : $ClangVer"
Write-Host "build dir: $BuildDir"
Write-Host "output : $OutputBin"
Write-Host "jobs : $Jobs"
Write-Host ""
if (Get-Command ccache -ErrorAction SilentlyContinue) {
Write-Host "clearing ccache..."
ccache -C -z
}
$env:CCACHE_DISABLE = "1"
$TestsFlag = if ($Full) { "ON" } else { "OFF" }
$ToolsFlag = if ($Full) { "ON" } else { "OFF" }
$ServerFlag = if ($Full) { "ON" } else { "OFF" }
cmake --fresh `
-S $RootDir `
-B $BuildDir `
-G "Ninja" `
-DCMAKE_BUILD_TYPE=Release `
-DCMAKE_C_COMPILER=clang `
-DCMAKE_CXX_COMPILER=clang++ `
-DCMAKE_C_FLAGS="-ftime-trace" `
-DCMAKE_CXX_FLAGS="-ftime-trace" `
-DGGML_CCACHE=OFF `
-DGGML_OPENMP=ON `
-DGGML_NATIVE=OFF `
"-DLLAMA_BUILD_TESTS=$TestsFlag" `
-DLLAMA_BUILD_EXAMPLES=OFF `
"-DLLAMA_BUILD_TOOLS=$ToolsFlag" `
"-DLLAMA_BUILD_SERVER=$ServerFlag" `
-DLLAMA_BUILD_APP=OFF
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$StrayTrace = Join-Path $RootDir "-.json"
if (Test-Path $StrayTrace) {
Remove-Item $StrayTrace -Force
}
Write-Host ""
Write-Host "Initializing ClangBuildAnalyzer..."
ClangBuildAnalyzer --start $BuildDir
Write-Host ""
Write-Host "building..."
Write-Host ""
$StartTime = Get-Date
cmake --build $BuildDir --clean-first -j $Jobs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$Elapsed = (Get-Date) - $StartTime
Write-Host ""
Write-Host ("build time: {0}s ({1}m {2}s)" -f [int]$Elapsed.TotalSeconds, [int]$Elapsed.TotalMinutes, $Elapsed.Seconds)
Write-Host ""
Write-Host "Aggregating profile metrics..."
ClangBuildAnalyzer --stop $BuildDir $OutputBin | Out-Null
Write-Host ""
Write-Host ("=" * 80)
$TUs = "?"
if (Test-Path $Report) {
$Match = Select-String -Path $Report -Pattern "Compilation \((\d+)" | Select-Object -First 1
if ($Match) { $TUs = $Match.Matches[0].Groups[1].Value }
}
ClangBuildAnalyzer --analyze $OutputBin | Tee-Object -FilePath $Report
Write-Host ""
Write-Host "translation units: $TUs"
Write-Host ""
Write-Host "largest trace files (top 20 by size):"
Get-ChildItem -Path $BuildDir -Recurse -Filter "*.json" |
Where-Object { $_.Name -ne "compile_commands.json" } |
Sort-Object Length -Descending |
Select-Object -First 20 |
ForEach-Object { "{0,8:F1} KB {1}" -f ($_.Length / 1024), $_.FullName }
Write-Host ""
Write-Host "ClangBuildAnalyzer report was generated: $Report"
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Compile-time profiling using clang -ftime-trace + ClangBuildAnalyzer.
#
# Usage:
# ./scripts/build-profile.sh [--full] [-jN]
#
# --full: include Server, Tools, and Tests (default: minimal build)
# -jN : number of parallel jobs (default: all cores)
#
# Requires ClangBuildAnalyzer:
# macOS: brew install clang-build-analyzer
# Linux: https://github.com/aras-p/ClangBuildAnalyzer.git
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
FULL=0
JOBS="-j$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
for arg in "$@"; do
case "${arg}" in
--full) FULL=1 ;;
-j*) JOBS="${arg}" ;;
*) echo "error: unknown argument: ${arg}" >&2; exit 1 ;;
esac
done
if [ "${FULL}" -eq 1 ]; then
BUILD_DIR="${ROOT_DIR}/build-profile-full"
REPORT="${BUILD_DIR}/profile-report-full.txt"
else
BUILD_DIR="${ROOT_DIR}/build-profile-baseline"
REPORT="${BUILD_DIR}/profile-report.txt"
fi
OUTPUT_BIN="${BUILD_DIR}/clang_analysis.bin"
if ! command -v clang++ &>/dev/null; then
echo "error: clang++ not found" >&2
exit 1
fi
if ! command -v ClangBuildAnalyzer &>/dev/null; then
echo "error: ClangBuildAnalyzer not found" >&2
echo " brew install clangbuildanalyzer (macOS)" >&2
echo " or: https://github.com/aras-p/ClangBuildAnalyzer/releases" >&2
exit 1
fi
CLANG_VER=$(clang++ --version | head -1)
echo "compiler : ${CLANG_VER}"
echo "build dir: ${BUILD_DIR}"
echo "output : ${OUTPUT_BIN}"
echo "jobs : ${JOBS}"
echo
if command -v ccache &>/dev/null; then
echo "clearing ccache..."
ccache -C -z
fi
export CCACHE_DISABLE=1
cmake --fresh \
-S "${ROOT_DIR}" \
-B "${BUILD_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_C_FLAGS="-ftime-trace" \
-DCMAKE_CXX_FLAGS="-ftime-trace" \
-DGGML_CCACHE=OFF \
-DGGML_OPENMP=ON \
-DGGML_NATIVE=OFF \
-DLLAMA_BUILD_TESTS=$([ "${FULL}" -eq 1 ] && echo ON || echo OFF) \
-DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_TOOLS=$([ "${FULL}" -eq 1 ] && echo ON || echo OFF) \
-DLLAMA_BUILD_SERVER=$([ "${FULL}" -eq 1 ] && echo ON || echo OFF) \
-DLLAMA_BUILD_APP=OFF
echo
echo "Initializing ClangBuildAnalyzer..."
ClangBuildAnalyzer --start "${BUILD_DIR}"
echo
echo "building..."
echo
START=$(date +%s)
cmake --build "${BUILD_DIR}" --clean-first "${JOBS}"
END=$(date +%s)
ELAPSED=$((END - START))
echo
printf "build time: %ds (%dm %ds)\n" "${ELAPSED}" "$((ELAPSED / 60))" "$((ELAPSED % 60))"
echo
echo "Aggregating profile metrics..."
ClangBuildAnalyzer --stop "${BUILD_DIR}" "${OUTPUT_BIN}" > /dev/null
echo
echo "================================================================================"
TUS=$(grep -oP "Compilation \(\K[0-9]+" "${REPORT}" 2>/dev/null || echo "?")
ClangBuildAnalyzer --analyze "${OUTPUT_BIN}" | tee "${REPORT}"
echo
echo "translation units: ${TUS}"
echo
echo "largest trace files (top 20 by size):"
find "${BUILD_DIR}" -name "*.json" ! -name "compile_commands.json" \
| xargs ls -l 2>/dev/null \
| awk 'NF>5 {print $5, $NF}' \
| sort -rn \
| awk 'NR<=20 {printf "%8.1f KB %s\n", $1/1024, $2}'
echo
echo "ClangBuildAnalyzer report was generated: ${REPORT}"
+105
View File
@@ -0,0 +1,105 @@
#!/bin/bash
# Delete GitHub Actions caches matching a key prefix, oldest first.
#
# Usage: ccache-clear.sh --key KEY [--older DURATION] [--min N] [--dry-run]
# --key: cache key prefix to match and delete (without the ccache- prefix)
# --older: only delete caches created more than DURATION ago (e.g. 5m, 1h, 1d);
# by default all matching caches are deleted
# --min: stop deleting if fewer than N caches would remain (default: 0)
# --dry-run: only print the caches that would be deleted, without deleting them
#
# Env (when running in GitHub Actions):
# GH_TOKEN: token for the gh CLI
# GITHUB_REPOSITORY: owner/repo of the caches to manage
set -euo pipefail
KEY=""
OLDER=""
MIN=0
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
--key) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 1; }; KEY="$2"; shift 2 ;;
--older) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 1; }; OLDER="$2"; shift 2 ;;
--min) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 1; }; MIN="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
*) echo "Unknown argument: $1"; exit 1 ;;
esac
done
command -v gh >/dev/null 2>&1 || { echo "Error: GitHub CLI (gh) is required" >&2; exit 1; }
[[ -n "${GITHUB_REPOSITORY:-}" ]] || { echo "Error: GITHUB_REPOSITORY not set" >&2; exit 1; }
[[ -n "$KEY" ]] || { echo "Error: --key is required" >&2; exit 1; }
[[ "$MIN" =~ ^[0-9]+$ ]] || { echo "Invalid min value: $MIN" >&2; exit 1; }
# Convert a duration (e.g. 90m, 1h, 1d, plain seconds) to seconds
to_seconds() {
local val="$1"
[[ "$val" =~ ^[0-9]+$ ]] && { echo "$val"; return 0; }
local num="${val%?}" unit="${val: -1}" mult
[[ "$num" =~ ^[0-9]+$ ]] || return 1
case "$unit" in
s) mult=1 ;;
m) mult=60 ;;
h) mult=3600 ;;
d) mult=86400 ;;
*) return 1 ;;
esac
echo $((num * mult))
}
# Convert an ISO-8601 UTC timestamp (e.g. 2026-08-23T16:51:23.313693Z) to epoch seconds
to_epoch() {
local val="$1" out
# GNU date (e.g. Linux)
if out=$(date -d "$val" +%s 2>/dev/null) && [[ "$out" =~ ^[0-9]+$ ]]; then
echo "$out"
return 0
fi
# BSD date (e.g. macOS); fractional seconds are not needed, TZ forces UTC
out=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "${val:0:19}" +%s 2>/dev/null) || return 1
[[ "$out" =~ ^[0-9]+$ ]] || return 1
echo "$out"
}
CACHES=$(gh cache list --repo "$GITHUB_REPOSITORY" --key "ccache-$KEY" --json id,key,createdAt --jq '.[] | [.createdAt, .id, .key] | @tsv' | LC_ALL=C sort)
if [[ -z "$CACHES" ]]; then
echo "No caches found with key prefix: $KEY"
exit 0
fi
TOTAL=$(( $(wc -l <<< "$CACHES") ))
echo "Found $TOTAL cache(s) with key prefix: $KEY (oldest first):"
while IFS=$'\t' read -r CREATED ID CACHE_KEY; do
printf ' %s %s %s\n' "$CREATED" "$ID" "$CACHE_KEY"
done <<< "$CACHES"
CUTOFF=""
if [[ -n "$OLDER" ]]; then
OLDER_SECONDS=$(to_seconds "$OLDER") || { echo "Invalid older value: $OLDER (expected e.g. 90m, 1h, 1d)" >&2; exit 1; }
CUTOFF=$(( $(date +%s) - OLDER_SECONDS ))
fi
# Caches are sorted oldest first
DELETED=0
while IFS=$'\t' read -r CREATED ID CACHE_KEY; do
if [[ -n "$CUTOFF" ]]; then
CREATED_SECONDS=$(to_epoch "$CREATED") || { echo "Failed to parse date: $CREATED" >&2; exit 1; }
if [[ "$CREATED_SECONDS" -ge "$CUTOFF" ]]; then
echo "Rest are not older than $OLDER, stopping"
break
fi
fi
if (( TOTAL - DELETED - 1 < MIN )); then
echo "Keeping at least $MIN cache(s), stopping"
break
fi
if [[ "$DRY_RUN" == "true" ]]; then
echo "Would delete cache: $ID ($CACHE_KEY)"
else
echo "Deleting cache: $ID ($CACHE_KEY)"
gh cache delete --repo "$GITHUB_REPOSITORY" "$ID"
fi
DELETED=$((DELETED + 1))
done <<< "$CACHES"
+292
View File
@@ -0,0 +1,292 @@
#!/bin/sh
# Check for backwards-incompatible API and ABI changes between two builds
#
# Backwards-incompatible API changes, such as removing a value from an enum,
# are checked by abi-compliance-checker. Such changes can break compilation of
# existing programs.
#
# Backwards-incompatible ABI changes, such as the removal of a public function,
# are checked by libigail-tools. Such changes could break run-time dynamic
# linking of existing binaries. (We don't use a-c-c for ABI checks because it
# needs a debug build, whereas abigail does not.)
#
# Commands:
# --generate <build-dir>: Creates API/ABI dumps in <build-dir>
# <build-dir> is expected to be a CMake build result
# --check <dir1> <dir2>: Compares dumps in <dir1> and <dir2>
# Comparison exit codes
# 0: all good
# 1: backwards-incompatible changes found
#
# Options:
# --include-path <dir>: a-c-c calls gcc on headers; use this option to add
# directories to gcc's search path
#
#
# This script would typically be used before cutting a release:
#
# 1. Generate API/ABI dump for the old version
#
# $ check-apiabi-compat.sh --generate <build-dir-old> libfoo [ libbar ...]
#
# 2. <update source>
#
# 3. Generate API/ABI dump for the new version
#
# $ check-apiabi-compat.sh --generate <build-dir-new> libfoo [ libbar ...]
#
# 4. Compare the two dumps
#
# $ check-apiabi-compat.sh --check <old-build-dir> <new-build-dir>
#
# If the check exits 0, all is fine. Otherwise, backwards-incompatible
# changes were found, and the librar(ies) need a SOVER bump.
set -eu
# Preconditions
if ! command -v abi-compliance-checker >/dev/null 2>&1; then
echo "abi-compliance-checker is not installed." >&2
exit 1
elif ! command -v abidw >/dev/null 2>&1; then
echo "abigail-tools are not installed." >&2
exit 1
fi
# Some generic functions
usage() {
echo "Usage: $0 [ --include-path <dir> ] --generate <build-dir> libXXX [ libYYY ... ]" >&2
echo " $0 [ --strict ] --check <old-build-dir> <new-build-dir>" >&2
echo "" >&2
echo " --strict: fail on any API/ABI change, including backwards-compatible additions" >&2
}
get_cmake_project_name() {
sed -nr 's/^project\("(.*)".*$/\1/p' CMakeLists.txt
}
get_cmake_version() {
major="$(sed -nr 's/^set\([A-Z]+_VERSION_MAJOR ([0-9]+)\)$/\1/p' CMakeLists.txt)"
minor="$(sed -nr 's/^set\([A-Z]+_VERSION_MINOR ([0-9]+)\)$/\1/p' CMakeLists.txt)"
patch="$(sed -nr 's/^set\([A-Z]+_VERSION_PATCH ([0-9]+)\)$/\1/p' CMakeLists.txt)"
echo "$major.$minor.$patch"
}
# Option parsing and validation
DO_GEN=0
DO_CHECK=0
STRICT=0
BUILD_DIR=
BUILD_DIR_NEW=
INCLUDE_PATHS=
while [ "$#" -gt 0 ]; do
case "$1" in
--generate=*)
DO_GEN=1
BUILD_DIR="${1#*=}"
shift
;;
--generate)
DO_GEN=1
if [ -z "${2:-}" ]; then
usage
exit 1
fi
BUILD_DIR="$2"
shift 2
;;
--check)
DO_CHECK=1
if [ -z "${2:-}" ] || [ -z "${3:-}" ]; then
usage
exit 1
elif ! [ -d "$2" ]; then
echo "$2 is not a directory." >&2
exit 1
elif ! [ -d "$3" ]; then
echo "$3 is not a directory." >&2
exit 1
fi
BUILD_DIR="$2"
BUILD_DIR_NEW="$3"
shift 3
;;
--include-path=*)
INCLUDE_PATHS="$INCLUDE_PATHS ${1#*=}"
shift
;;
--include-path)
if [ -z "${2:-}" ]; then
usage
exit 1
fi
INCLUDE_PATHS="$INCLUDE_PATHS $2"
shift 2
;;
--strict)
STRICT=1
shift
;;
-h | --help)
usage
exit 1
;;
-?*)
usage
exit 1
;;
*)
break
;;
esac
done
if [ $((DO_GEN + DO_CHECK)) -gt 1 ]; then
echo "Can only use one --command." >&2
exit
fi
PROJECT_NAME="$(get_cmake_project_name)"
PROJECT_VERSION="$(get_cmake_version)"
LIB_NAMES=""
while [ "$#" -gt 0 ]; do
if [ "${1#lib}" = "$1" ]; then
echo "Library to check must start with libXXX." >&2
exit 1
fi
LIB_NAMES="$LIB_NAMES $1"
shift
done
dump_current_api() {
echo "Dumping API..."
DESCRIPTOR="$BUILD_DIR/apiabi/acc-descriptor.xml"
mkdir -p "$BUILD_DIR/apiabi"
cat >"$DESCRIPTOR" <<EOF
<version>$PROJECT_VERSION</version>
<headers>include</headers>
<add_include_paths>$INCLUDE_PATHS</add_include_paths>
EOF
# This addresses a bug between a-c-c and universal-ctags, manifested when
# a name is use both for a tag and a function name
mkdir -p "$BUILD_DIR/apiabi/.ctags.d"
echo "--fields=-t" >"$BUILD_DIR/apiabi/.ctags.d/acc.ctags"
# Change HOME so that .ctags.d gets picked up by universal-ctags, if used
HOME="$BUILD_DIR/apiabi" abi-compliance-checker \
-headers-only \
-lib "$PROJECT_NAME" \
-dump "$DESCRIPTOR" \
-log-path "$BUILD_DIR/apiabi/acc.log" \
-dump-path "$BUILD_DIR/apiabi/api.dump"
# acc generates this file with an ancient timestamp, which confuses gzip
touch "$BUILD_DIR/apiabi/api.dump"
}
dump_current_abi() {
echo "Dumping ABIs ..."
mkdir -p "$BUILD_DIR/apiabi"
# The suppressions are needed to avoid including all the internal C++
# symbols, and system types
cat >"$BUILD_DIR/apiabi/abidw.suppress" <<EOF
[suppress_function]
label = suppress internal C++ mangled functions
symbol_name_regexp = ^_Z
drop = yes
[suppress_variable]
label = suppress internal C++ mangled variables
symbol_name_regexp = ^_Z
drop = yes
[suppress_type]
label = Suppress types outside of our own source
source_location_not_regexp = ^include/
drop = yes
EOF
# In abidw 2.5, handling of undefined stuff was changed a bit
abidw_version="$(abidw --version | sed -r 's/^abidw: ([0-9]+\.[0-9]+).*$/\1/')"
abidw_major="${abidw_version%.*}"
abidw_minor="${abidw_version#*.}"
if [ "$abidw_major" -gt 2 ] || [ "$abidw_minor" -gt 4 ]; then
abidw_undefined_syms_options="--no-load-undefined-interfaces"
else
abidw_undefined_syms_options="--drop-undefined-syms"
fi
for lib_name in $LIB_NAMES; do
# Depending on where add_library resides, the libraries can end up in
# build/src or build/bin
lib_path="$BUILD_DIR/src/$lib_name.so"
if ! [ -f "$lib_path" ]; then
lib_path="$BUILD_DIR/bin/$lib_name.so"
if ! [ -f "$lib_path" ]; then
echo "Cannot find library $lib_name.so" >&2
exit 1
fi
fi
abidw \
--headers-dir include \
"$abidw_undefined_syms_options" \
--suppressions "$BUILD_DIR/apiabi/abidw.suppress" \
--out-file "${BUILD_DIR}/apiabi/$lib_name.abi.xml" \
"$lib_path"
done
}
# Run the actual commands
if [ "$DO_GEN" -eq 1 ]; then
dump_current_api
dump_current_abi
exit 0
elif [ "$DO_CHECK" -eq 1 ]; then
# From here on, we don't want to exit on first error
set +e
ACC_STRICT=
[ "$STRICT" -eq 0 ] || ACC_STRICT="-strict"
abi-compliance-checker \
$ACC_STRICT \
-source \
-library "$PROJECT_NAME" \
-old "$BUILD_DIR/apiabi/api.dump" \
-new "$BUILD_DIR_NEW/apiabi/api.dump" \
-src-report-path "$BUILD_DIR_NEW/apiabi/api_compat_report.html"
API_RESULT=$?
ABI_RESULT=0
for xml_file in "$BUILD_DIR/apiabi/"lib*.abi.xml; do
xml_file_new="$BUILD_DIR_NEW/apiabi/$(basename "$xml_file")"
if ! [ -f "$xml_file_new" ]; then
echo "Cannot compare, missing file: $xml_file_new" >&2
exit 1
fi
abidiff "$xml_file" "$xml_file_new"
res=$?
[ "$((res & 8))" -ne 0 ] && ABI_RESULT=1
# check bit 2 for compatible ABI changes (like new symbols) and if
# STRICT is set then handle this as an error.
[ "$STRICT" -eq 1 ] && [ "$((res & 4))" -ne 0 ] && ABI_RESULT=1
done
if [ "$API_RESULT" -gt 0 ]; then
if [ "$STRICT" -eq 1 ]; then
echo "ERROR: API changed — a minor version bump is required." >&2
else
echo "ERROR: API changed with backwards-incompatible problems — a major version bump is required." >&2
fi
fi
if [ "$ABI_RESULT" -gt 0 ]; then
if [ "$STRICT" -eq 1 ]; then
echo "ERROR: ABI changed — a minor version bump is required." >&2
else
echo "ERROR: ABI changed with backwards-incompatible problems — a major version bump is required." >&2
fi
fi
if [ "$((API_RESULT + ABI_RESULT))" -gt 0 ]; then
exit 1
fi
fi
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
# Check API/ABI compatibility between the previous release tag and current HEAD.
#
# Finds the most recent vX.Y.Z tag, checks it out in a temporary git worktree,
# builds both versions with shared libs enabled, and uses check-apiabi-compat.sh
# to compare the results.
#
# Exit codes:
# 0: compatible, or check was skipped
# 1: backwards-incompatible changes found, or build failed
#
# Options:
# --tag <version>: compare against this tag instead of the latest release
#
# Environment:
# SKIP_APIABI_CHECK: set to 1 or true to skip
# APIABI_COMPARE_TAG: equivalent to --tag (used by CI)
# APIABI_REPORT_DIR: preserve reports and dumps in this directory, even if the comparison fails
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
LIBS=(libllama libmtmd)
usage() {
echo "Usage: $0 [--tag <version>]" >&2
echo " --tag <version> Compare against this release tag (default: latest)" >&2
}
COMPARE_TAG="${APIABI_COMPARE_TAG:-}"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--tag)
if [[ -z "${2:-}" ]]; then usage; exit 1; fi
COMPARE_TAG="$2"
shift 2
;;
--tag=*)
COMPARE_TAG="${1#*=}"
shift
;;
-h | --help)
usage; exit 0
;;
*)
usage; exit 1
;;
esac
done
if [[ "${SKIP_APIABI_CHECK:-}" == "1" || "${SKIP_APIABI_CHECK:-}" == "true" ]]; then
echo "SKIP_APIABI_CHECK is set - skipping API/ABI compatibility check"
exit 0
fi
if ! command -v abi-compliance-checker >/dev/null 2>&1 || ! command -v abidw >/dev/null 2>&1; then
echo "Warning: abi-compliance-checker or abigail-tools not installed - skipping API/ABI check"
exit 0
fi
if [[ -n "${COMPARE_TAG}" ]]; then
PREV_TAG="${COMPARE_TAG}"
if ! git -C "$REPO_ROOT" rev-parse --verify "${PREV_TAG}^{}" >/dev/null 2>&1; then
echo "Error: tag '${PREV_TAG}' not found in repository." >&2
exit 1
fi
else
PREV_TAG=$(git -C "$REPO_ROOT" tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)
if [[ -z "${PREV_TAG}" ]]; then
echo "Warning: no previous release tag found - skipping API/ABI check"
exit 0
fi
fi
OLD_VERSION="${PREV_TAG#v}"
OLD_MAJOR="${OLD_VERSION%%.*}"
OLD_MINOR="${OLD_VERSION#*.}"; OLD_MINOR="${OLD_MINOR%%.*}"
NEW_MAJOR=$(grep "set(LLAMA_VERSION_MAJOR" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*MAJOR \([0-9]*\).*/\1/')
NEW_MINOR=$(grep "set(LLAMA_VERSION_MINOR" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*MINOR \([0-9]*\).*/\1/')
if [[ "$NEW_MAJOR" -gt "$OLD_MAJOR" ]]; then
echo "Major version increment ($OLD_MAJOR -> $NEW_MAJOR): API/ABI breaking changes are expected, skipping compatibility check."
exit 0
fi
CHECK_FLAGS=()
if [[ "$NEW_MINOR" -eq "$OLD_MINOR" ]]; then
echo "Patch version bump detected: checking for any API/ABI changes (a minor bump is required if any are found)..."
CHECK_FLAGS+=(--strict)
else
echo "Minor version bump detected: checking for backwards-incompatible API/ABI changes..."
fi
echo "Checking API/ABI compatibility against ${PREV_TAG}..."
WORKTREE_DIR=$(mktemp -d)
BUILD_OLD=$(mktemp -d)
BUILD_NEW=$(mktemp -d)
cleanup() {
git -C "$REPO_ROOT" worktree remove --force "$WORKTREE_DIR" 2>/dev/null || true
rm -rf "$WORKTREE_DIR" "$BUILD_OLD" "$BUILD_NEW"
}
trap cleanup EXIT
git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" "$PREV_TAG"
cmake -S "$WORKTREE_DIR" -B "$BUILD_OLD" -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build "$BUILD_OLD" --parallel "$(nproc)"
cmake -S "$REPO_ROOT" -B "$BUILD_NEW" -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build "$BUILD_NEW" --parallel "$(nproc)"
(cd "$WORKTREE_DIR" && "$SCRIPT_DIR/check-apiabi-compat.sh" --include-path ggml/include --generate "$BUILD_OLD" "${LIBS[@]}")
(cd "$REPO_ROOT" && "$SCRIPT_DIR/check-apiabi-compat.sh" --include-path ggml/include --generate "$BUILD_NEW" "${LIBS[@]}")
CHECK_RESULT=0
(cd "$REPO_ROOT" && "$SCRIPT_DIR/check-apiabi-compat.sh" "${CHECK_FLAGS[@]}" --check "$BUILD_OLD" "$BUILD_NEW") || CHECK_RESULT=$?
if [[ -n "${APIABI_REPORT_DIR:-}" ]]; then
mkdir -p "$APIABI_REPORT_DIR/old" "$APIABI_REPORT_DIR/new"
cp -R "$BUILD_OLD/apiabi/." "$APIABI_REPORT_DIR/old/"
cp -R "$BUILD_NEW/apiabi/." "$APIABI_REPORT_DIR/new/"
echo "API/ABI reports saved to $APIABI_REPORT_DIR"
fi
exit "$CHECK_RESULT"
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -euo pipefail
#
# check-requirements.sh checks all requirements files for each top-level
# convert*.py script.
#
# WARNING: This is quite IO intensive, because a fresh venv is set up for every
# python script. As of 2023-12-22, this writes ~2.7GB of data. An adequately
# sized tmpfs /tmp or ramdisk is recommended if running this frequently.
#
# usage: check-requirements.sh [<working_dir>]
# check-requirements.sh nocleanup [<working_dir>]
#
# where:
# - <working_dir> is a directory that can be used as the base for
# setting up the venvs. Defaults to `/tmp`.
# - 'nocleanup' as the first argument will disable automatic cleanup
# of the files created by this script.
#
# requires:
# - bash >= 3.2.57
# - shellcheck
#
# For each script, it creates a fresh venv, `pip install`s the requirements, and
# finally imports the python script to check for `ImportError`.
#
log() {
local level=$1 msg=$2
printf >&2 '%s: %s\n' "$level" "$msg"
}
debug() {
log DEBUG "$@"
}
info() {
log INFO "$@"
}
fatal() {
log FATAL "$@"
exit 1
}
cleanup() {
if [[ -n ${workdir+x} && -d $workdir && -w $workdir ]]; then
info "Removing $workdir"
local count=0
rm -rfv -- "$workdir" | while read -r; do
if (( count++ > 750 )); then
printf .
count=0
fi
done
printf '\n'
info "Removed $workdir"
fi
}
do_cleanup=1
if [[ ${1-} == nocleanup ]]; then
do_cleanup=0; shift
fi
if (( do_cleanup )); then
trap exit INT TERM
trap cleanup EXIT
fi
this=$(realpath -- "$0"); readonly this
cd "$(dirname "$this")/.." # PWD should stay in llama.cpp project directory
shellcheck "$this"
readonly reqs_dir=requirements
if [[ ${1+x} ]]; then
tmp_dir=$(realpath -- "$1")
if [[ ! ( -d $tmp_dir && -w $tmp_dir ) ]]; then
fatal "$tmp_dir is not a writable directory"
fi
else
tmp_dir=/tmp
fi
workdir=$(mktemp -d "$tmp_dir/check-requirements.XXXX"); readonly workdir
info "Working directory: $workdir"
check_requirements() {
local reqs=$1
info "$reqs: beginning check"
pip --disable-pip-version-check install -qr "$reqs"
info "$reqs: OK"
}
check_convert_script() {
local py=$1 # e.g. ./convert_hf_to_gguf.py
local pyname=${py##*/} # e.g. convert_hf_to_gguf.py
pyname=${pyname%.py} # e.g. convert_hf_to_gguf
info "$py: beginning check"
local reqs="$reqs_dir/requirements-$pyname.txt"
if [[ ! -r $reqs ]]; then
fatal "$py missing requirements. Expected: $reqs"
fi
# Check that all sub-requirements are added to top-level requirements.txt
if ! grep -qF "$reqs" requirements.txt; then
fatal "$reqs needs to be added to requirements.txt"
fi
local venv="$workdir/$pyname-venv"
python3 -m venv "$venv"
(
# shellcheck source=/dev/null
source "$venv/bin/activate"
check_requirements "$reqs"
python - "$py" "$pyname" <<'EOF'
import sys
from importlib.machinery import SourceFileLoader
py, pyname = sys.argv[1:]
SourceFileLoader(pyname, py).load_module()
EOF
)
if (( do_cleanup )); then
rm -rf -- "$venv"
fi
info "$py: imports OK"
}
readonly ignore_eq_eq='check_requirements: ignore "=="'
for req in */**/requirements*.txt; do
# Make sure exact release versions aren't being pinned in the requirements
# Filters out the ignore string
if grep -vF "$ignore_eq_eq" "$req" | grep -q '=='; then
tab=$'\t'
cat >&2 <<EOF
FATAL: Avoid pinning exact package versions. Use '~=' instead.
You can suppress this error by appending the following to the line:
$tab# $ignore_eq_eq
EOF
exit 1
fi
done
all_venv="$workdir/all-venv"
python3 -m venv "$all_venv"
(
# shellcheck source=/dev/null
source "$all_venv/bin/activate"
check_requirements requirements.txt
)
if (( do_cleanup )); then
rm -rf -- "$all_venv"
fi
check_convert_script examples/convert_legacy_llama.py
for py in convert_*.py; do
# skip convert_hf_to_gguf_update.py
# TODO: the check is failing for some reason:
# https://github.com/ggml-org/llama.cpp/actions/runs/8875330981/job/24364557177?pr=6920
[[ $py == convert_hf_to_gguf_update.py ]] && continue
check_convert_script "$py"
done
info 'Done! No issues found.'
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# List GitHub Actions workflow runs that are not finished yet, grouped by status.
set -euo pipefail
usage() {
echo "usage: $(basename "$0") [-r owner/repo] [-s status]... [-b branch] [-w regex] [-t regex] [-f regex] [--kill [-y]]"
echo " -r, --repo repository (default: current)"
echo " -s, --status run status to include, repeatable (default: the not-finished ones)"
echo " -b, --branch branch to fetch runs for; exact match, handled by the API"
echo " -w, --workflow case-insensitive regex matched against the workflow name (client-side)"
echo " -t, --title case-insensitive regex matched against the run title (client-side)"
echo " -f, --filter case-insensitive regex matched against the branch name (client-side)"
echo " --kill cancel the selected runs (requires -b, -w, -t or -f)"
echo " -y, --yes skip the confirmation prompt of --kill"
}
repo=""
branch=""
workflow=""
title=""
filter=""
kill=false
assume_yes=false
statuses=()
need_value() { echo "missing value for $1" >&2; exit 1; }
while [[ $# -gt 0 ]]; do
case "$1" in
-r|--repo) [[ $# -ge 2 ]] || need_value "$1"; repo="$2"; shift 2 ;;
-s|--status) [[ $# -ge 2 ]] || need_value "$1"; statuses+=("$2"); shift 2 ;;
-b|--branch) [[ $# -ge 2 ]] || need_value "$1"; branch="$2"; shift 2 ;;
-w|--workflow) [[ $# -ge 2 ]] || need_value "$1"; workflow="$2"; shift 2 ;;
-t|--title) [[ $# -ge 2 ]] || need_value "$1"; title="$2"; shift 2 ;;
-f|--filter) [[ $# -ge 2 ]] || need_value "$1"; filter="$2"; shift 2 ;;
--kill) kill=true; shift ;;
-y|--yes) assume_yes=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 1 ;;
esac
done
# one request per status: the API does not accept multiple statuses at once
[[ ${#statuses[@]} -gt 0 ]] || statuses=(in_progress queued pending waiting requested)
[[ -n "$repo" ]] || repo=$(gh repo view --json nameWithOwner -q .nameWithOwner)
query=("repos/$repo/actions/runs" -f per_page=100)
[[ -z "$branch" ]] || query+=(-f branch="$branch")
selected=$(
for status in "${statuses[@]}"; do
gh api --paginate --method GET "${query[@]}" -f status="$status" -q '.workflow_runs[]'
done | jq -sr --arg branch "$branch" --arg workflow "$workflow" --arg title "$title" --arg filter "$filter" '
unique_by(.id)
| map(
.age = ((now - (.run_started_at // .created_at | fromdate)) / 60 | floor)
| select(
($branch == "" or .head_branch == $branch) and
($workflow == "" or (.name | test($workflow; "i"))) and
($title == "" or (.display_title | test($title; "i"))) and
($filter == "" or (.head_branch | test($filter; "i")))
)
)
| sort_by(.status, -.age)'
)
tsv=$(jq -r '.[] | [ .status, (.age | tostring) + "m", .name, .head_branch, .display_title, .html_url ] | @tsv' <<< "$selected")
if [[ -z "$tsv" ]]; then
echo "no active runs in $repo"
exit 0
fi
printf 'STATUS\tAGE\tWORKFLOW\tBRANCH\tTITLE\tURL\n%s\n' "$tsv" | column -t -s $'\t'
echo
printf '%s:' "$repo"
for status in "${statuses[@]}"; do
printf ' %s=%s' "$status" "$(awk -F'\t' -v s="$status" '$1 == s' <<< "$tsv" | wc -l | tr -d ' ')"
done
echo
if [[ "$kill" == false ]]; then
exit 0
fi
if [[ -z "$branch$workflow$title$filter" ]]; then
echo "refusing to cancel everything: pass -b, -w, -t or -f to select the runs to cancel" >&2
exit 1
fi
count=$(wc -l <<< "$tsv" | tr -d ' ')
if [[ "$assume_yes" == false ]]; then
printf 'cancel %s run(s)? [y/N] ' "$count"
read -r reply </dev/tty || { echo; echo "aborted: no terminal to confirm on" >&2; exit 1; }
[[ "$reply" == [yY]* ]] || { echo "aborted"; exit 1; }
fi
jq -r '.[] | [ .id, .status ] | @tsv' <<< "$selected" |
while IFS=$'\t' read -r id status; do
gh run cancel "$id" --repo "$repo" || echo "failed to cancel run $id (status: $status)"
done
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
if [ $# -lt 2 ]; then
echo "usage: ./scripts/compare-commits.sh <commit1> <commit2> [tool] [additional arguments]"
echo " tool: 'llama-bench' (default) or 'test-backend-ops'"
echo " additional arguments: passed to the selected tool"
exit 1
fi
set -e
set -x
# Parse arguments
commit1=$1
commit2=$2
tool=${3:-llama-bench}
additional_args="${@:4}"
# Validate tool argument
if [ "$tool" != "llama-bench" ] && [ "$tool" != "test-backend-ops" ]; then
echo "Error: tool must be 'llama-bench' or 'test-backend-ops'"
exit 1
fi
# verify at the start that the compare script has all the necessary dependencies installed
./scripts/compare-llama-bench.py --check
if ! command -v sqlite3 >/dev/null 2>&1; then
echo "Error: sqlite3 is not installed or not in PATH"
echo "Please install sqlite3 to use this script"
exit 1
fi
if [ "$tool" = "llama-bench" ]; then
db_file="llama-bench.sqlite"
target="llama-bench"
run_args="-o sql -oe md $additional_args"
else # test-backend-ops
db_file="test-backend-ops.sqlite"
target="test-backend-ops"
run_args="perf --output sql $additional_args"
fi
rm -f "$db_file" > /dev/null
# to test a backend, call the script with the corresponding environment variable (e.g. GGML_CUDA=1 ./scripts/compare-commits.sh ...)
if [ -n "$GGML_CUDA" ]; then
CMAKE_OPTS="${CMAKE_OPTS} -DGGML_CUDA=ON"
fi
dir="build-bench"
function run {
rm -fr ${dir} > /dev/null
cmake -B ${dir} -S . ${CMAKE_OPTS} > /dev/null
cmake --build ${dir} -t $target -j $(nproc) > /dev/null
${dir}/bin/$target $run_args | sqlite3 "$db_file"
}
git checkout $commit1 > /dev/null
run
git checkout $commit2 > /dev/null
run
./scripts/compare-llama-bench.py -b $commit1 -c $commit2 --tool $tool -i "$db_file"
File diff suppressed because it is too large. Load diff
+279
View File
@@ -0,0 +1,279 @@
import argparse
import requests
import json
from pathlib import Path
import logging
logger = logging.getLogger("compare-logprobs")
logging.basicConfig(level=logging.INFO)
DESCRIPTION = """
Compare logits between llama.cpp and another inference engine using OpenAI-compatible server endpoints.
Unlike compare-logits.py, it allows dumping logits from a hosted API endpoint. Useful when it's not possible to run both models locally.
Example usage:
Step 1: Dump logits from two different servers
python scripts/compare-logprobs.py dump logits_llama.log http://localhost:9931/v1/completions
python scripts/compare-logprobs.py dump logits_other.log http://other-engine/v1/completions
(optionally, you can add --api-key <key> if the endpoint requires authentication)
Step 2: Compare the dumped logits
python scripts/compare-logprobs.py compare logits_llama.log logits_other.log report.md
"""
def get_remote_corpus(url: str, length: int) -> list[str]:
response = requests.get(url)
response.raise_for_status()
corpus = response.text
words = [w.strip() for w in corpus.strip().split(" ")]
words = [w for w in words if "<" not in w] # make sure nothing looks like special tokens
words = [w for w in words if len(w) > 0] # filter out empty strings
while len(words) < length:
words += words
return words[:length]
def dump_logits(
endpoint: str,
output_path: Path,
input_words: list[str],
pattern: list[tuple[bool, int]],
api_key=None,
):
logger.info(f"Dumping logits to {output_path} from endpoint {endpoint}...")
words = input_words
curr_text = ""
n_total = sum(n for get, n in pattern if get)
n_done = 0
i_cur = 0
i_total = len(words)
with output_path.open("w") as f:
for get, n in pattern:
if not get:
# skip n words
for i in range(n):
curr_text += words.pop(0) + " "
i_cur += 1
continue
# get n words
for i in range(n):
curr_text += words.pop(0) + " "
payload = {
"prompt": curr_text.strip(),
"temperature": 0.0,
"top_k": 1,
"max_tokens": 1,
"logprobs": 1,
"stream": False,
}
response = requests.post(
endpoint,
json=payload,
headers={"Authorization": f"Bearer {api_key}"} if api_key else {},
)
response.raise_for_status()
data = response.json()
data["__index"] = i_cur # add index for easier debugging later
data = json.dumps(data)
f.write(f"{data}\n")
n_done += 1
i_cur += 1
logger.info(
f"\n\n{data}\n\n[Step: {n_done}/{n_total} | Word: {i_cur}/{i_total}]"
)
logger.info(f"Logits dumped to {output_path}")
def get_token_logprobs(data: dict):
logprobs = data["choices"][0]["logprobs"]
if "content" in logprobs:
# llama.cpp case
top = logprobs["content"][0]["top_logprobs"][0]
return top["token"], top["logprob"]
else:
# vllm case
tokens = logprobs["tokens"]
token_logprobs = logprobs["token_logprobs"]
return tokens[0], token_logprobs[0]
def clean_text(text: str) -> str:
return (
"'"
+ text.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\r", "\\r")
.replace("|", "\\|")
+ "'"
)
def compare_logits(input1: Path, input2: Path, output_path: Path):
with input1.open("r") as f1, input2.open("r") as f2, output_path.open("w") as fout:
lines1 = f1.readlines()
lines2 = f2.readlines()
tab_header = [
"idx",
input1.name,
"logprob_1",
input2.name,
"logprob_2",
"diff (abs)",
]
tab_entries = []
tab_max_widths = [len(h) for h in tab_header]
assert len(lines1) == len(
lines2
), "Input files must have the same number of lines."
fout.write("# Logits Comparison Report\n\n")
for i, (line1, line2) in enumerate(zip(lines1, lines2)):
if not line1.strip() or not line2.strip():
continue # skip empty lines
data1 = json.loads(line1)
data2 = json.loads(line2)
idx1 = data1.get("__index", -1)
idx2 = data2.get("__index", -1)
if idx1 != idx2:
logger.warning(
f"Warning: Mismatched indices at line {i}: {idx1} vs {idx2}"
)
token1, logprob1 = get_token_logprobs(data1)
token2, logprob2 = get_token_logprobs(data2)
token1 = clean_text(token1)
token2 = clean_text(token2)
abs_diff = abs(logprob1 - logprob2)
tab_entries.append(
(
str(idx1 + 1),
token1,
f"{logprob1:.4f}",
token2,
f"{logprob2:.4f}",
f"{(abs_diff):.4f}",
)
)
for i in range(len(tab_entries)):
for j in range(len(tab_header)):
tab_max_widths[j] = max(tab_max_widths[j], len(tab_entries[i][j]))
output = ""
for j in range(len(tab_header)):
output += f"| {tab_header[j]:<{tab_max_widths[j]}} "
output += "|\n"
for j in range(len(tab_header)):
output += f"|{'-' * (tab_max_widths[j] + 2)}"
output += "|\n"
for entry in tab_entries:
for j in range(len(tab_header)):
output += f"| {entry[j]:<{tab_max_widths[j]}} "
output += "|\n"
logger.info("\n" + output)
fout.write(output)
logger.info(f"Report written to {output_path}")
def parse_pattern(pattern: str) -> list[tuple[bool, int]]:
parts = pattern.split(",")
result = []
for i, part in enumerate(parts):
n = int(part)
if i % 2 == 0:
result.append((True, n)) # get n words
else:
result.append((False, n)) # skip n words
return result
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter
)
subparsers = parser.add_subparsers(
dest="verb", required=True, help="action to perform"
)
# dump subcommand
parser_dump = subparsers.add_parser("dump", help="dump logits from an endpoint")
parser_dump.add_argument(
"output", type=Path, help="output path for dumped logits (.log)"
)
parser_dump.add_argument(
"endpoint", type=str, help="OAI-compat /completions endpoint"
)
parser_dump.add_argument(
"--api-key",
type=str,
default=None,
help="API key for authentication (if required)",
)
parser_dump.add_argument(
"--file",
type=str,
default="https://raw.githubusercontent.com/ggml-org/llama.cpp/eaba92c3dcc980ebe753348855d4a5d75c069997/tools/server/README.md",
help="File containing prompt to use instead of the default (can also be an URL)",
)
parser_dump.add_argument(
"--pattern",
type=str,
default="10,1000,10,4000,10",
help="Pattern n_get,n_skip,... where n_get is number of words to get and n_skip is number of words to skip (num of words, NOT num of tokens)",
)
# compare subcommand
parser_compare = subparsers.add_parser(
"compare", help="compare two dumped logits files"
)
parser_compare.add_argument("input1", type=Path, help="first input file (.log)")
parser_compare.add_argument("input2", type=Path, help="second input file (.log)")
parser_compare.add_argument(
"output", type=Path, help="output path for comparison report (.md)"
)
try:
return parser.parse_args()
except Exception as e:
parser.print_help()
raise e
def main():
args = parse_args()
if args.verb == "dump":
pattern = parse_pattern(args.pattern)
required_words = sum(n for _, n in pattern)
if args.file.startswith("http"):
input_words = get_remote_corpus(args.file, required_words)
logger.info(f"Fetched {len(input_words)} words from remote {args.file}")
else:
with open(args.file, "r") as f:
input_words = f.read().strip().split(" ")
input_words = [w for w in input_words if len(w) > 0] # filter out empty strings
if len(input_words) < required_words:
raise ValueError(
f"Input file has only {len(input_words)} words, but pattern requires at least {required_words} words."
)
logger.info(f"Using {len(input_words)} words")
dump_logits(args.endpoint, args.output, input_words, pattern, args.api_key)
elif args.verb == "compare":
compare_logits(args.input1, args.input2, args.output)
else:
raise ValueError(f"Unknown verb: {args.verb}")
if __name__ == "__main__":
main()
+86
View File
@@ -0,0 +1,86 @@
# download the prebuilt llama-connect binary for the given platform
# the release assets are made by: https://github.com/ggml-org/llama-connect/blob/master/.github/workflows/build.yml
cmake_minimum_required(VERSION 3.18)
set(REPO "" CACHE STRING "GitHub repository to download from (owner/name)")
set(VERSION "" CACHE STRING "Release tag to download, or 'latest'")
set(PLATFORM "" CACHE STRING "Platform suffix of the release asset (ex: linux-x64)")
set(OUT_DIR "" CACHE STRING "Directory to place the binary into")
set(WORK_DIR "" CACHE STRING "Scratch directory used for the download")
if (PLATFORM MATCHES "^win-")
set(ASSET "llama-connect-${PLATFORM}.zip")
set(BINARY "llama-connect.exe")
else()
set(ASSET "llama-connect-${PLATFORM}.tar.gz")
set(BINARY "llama-connect")
endif()
if (VERSION STREQUAL "latest")
set(URL "https://github.com/${REPO}/releases/latest/download/${ASSET}")
else()
set(URL "https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}")
endif()
set(DST "${OUT_DIR}/${BINARY}")
set(ARCHIVE "${WORK_DIR}/${ASSET}")
set(STAMP "${WORK_DIR}/${BINARY}.url")
# skip if already downloaded from the same URL
if (EXISTS "${DST}" AND EXISTS "${STAMP}")
file(READ "${STAMP}" prev_url)
if (prev_url STREQUAL "${URL}")
return()
endif()
endif()
file(REMOVE "${STAMP}")
file(MAKE_DIRECTORY "${WORK_DIR}")
message(STATUS "llama-connect: downloading ${URL}")
# retry a few times, this runs in CI where a transient failure must not break the build
foreach (attempt RANGE 1 3)
file(DOWNLOAD "${URL}" "${ARCHIVE}" STATUS status TLS_VERIFY ON INACTIVITY_TIMEOUT 60)
list(GET status 0 code)
list(GET status 1 msg)
if (code EQUAL 0)
break()
endif()
message(STATUS "llama-connect: download attempt ${attempt} failed (${code}: ${msg})")
file(REMOVE "${ARCHIVE}")
endforeach()
if (NOT code EQUAL 0)
file(REMOVE "${ARCHIVE}")
message(FATAL_ERROR
"llama-connect: failed to download ${URL} (${code}: ${msg})\n"
" use -DLLAMA_CONNECT_VERSION=<tag> to select another release, "
"or -DLLAMA_CONNECT=OFF to skip this tool")
endif()
set(EXTRACT_DIR "${WORK_DIR}/extract")
file(REMOVE_RECURSE "${EXTRACT_DIR}")
file(MAKE_DIRECTORY "${EXTRACT_DIR}")
file(ARCHIVE_EXTRACT INPUT "${ARCHIVE}" DESTINATION "${EXTRACT_DIR}")
if (NOT EXISTS "${EXTRACT_DIR}/${BINARY}")
message(FATAL_ERROR "llama-connect: ${ASSET} does not contain ${BINARY}")
endif()
file(MAKE_DIRECTORY "${OUT_DIR}")
file(COPY "${EXTRACT_DIR}/${BINARY}" DESTINATION "${OUT_DIR}"
FILE_PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
)
file(WRITE "${STAMP}" "${URL}")
message(STATUS "llama-connect: ready at ${DST}")
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
This script parses docs/ops/*.csv and creates the ops.md, which is a table documenting supported operations on various ggml backends.
"""
import csv
import logging
import sys
from pathlib import Path
from collections import defaultdict
class DocsGenerator:
def __init__(self, ggml_root: str, output_filename: str = "ops.md"):
self.ggml_root = Path(ggml_root)
self.ops_dir = self.ggml_root / "docs" / "ops"
self.output_filename = output_filename
self.backend_support: dict[str, dict[str, list[bool]]] = defaultdict(
lambda: defaultdict(list)
)
self.all_operations: set[str] = set()
self.all_backends: set[str] = set()
self.logger = logging.getLogger(__name__)
def parse_support_files(self) -> None:
if not self.ops_dir.exists():
self.logger.warning(f"ops directory not found: {self.ops_dir}")
return
self.logger.info(f"Parsing support files from {self.ops_dir}...")
for support_file in self.ops_dir.glob("*.csv"):
self.logger.info(f" Reading: {support_file.name}")
self._parse_support_file(support_file)
def _parse_support_file(self, file_path: Path) -> None:
try:
with open(file_path, "r", newline='') as f:
reader = csv.DictReader(f)
for row in reader:
# Skip rows that don't have support mode
if row.get('test_mode') != 'support':
continue
backend_name = row.get('backend_name', '').strip()
operation = row.get('op_name', '').strip()
supported_str = row.get('error_message', '').strip() # "yes" or "no"
backend_reg_name = row.get('backend_reg_name', '').strip()
# Skip invalid or error operations
if not operation or not backend_name or operation in [
"CONTEXT_ERROR",
"BUILD_ERROR",
]:
continue
is_supported = supported_str.lower() == "yes"
# Use backend_reg_name for grouping, fallback to backend_name
backend_key = backend_reg_name if backend_reg_name else backend_name
self.all_backends.add(backend_key)
self.backend_support[backend_key][operation].append(is_supported)
self.all_operations.add(operation)
except Exception as e:
self.logger.error(f" Error parsing {file_path}: {e}")
def get_backend_support_status(self, backend: str, operation: str) -> str:
support_list = self.backend_support[backend].get(operation, [])
if not support_list:
return "unsupported"
all_supported = all(support_list)
any_supported = any(support_list)
if all_supported:
return "supported"
elif any_supported:
return "partially supported"
else:
return "unsupported"
def get_support_status(self, operation: str) -> str:
if operation not in self.all_operations:
return "unsupported"
support_count = 0
total_backends = len(self.all_backends)
for backend in self.all_backends:
if self.backend_support[backend].get(operation, False):
support_count += 1
if support_count == 0:
return "unsupported"
elif support_count == total_backends:
return "supported"
else:
return "partially supported"
def get_support_symbol(self, status: str) -> str:
symbols = {"supported": "✅", "partially supported": "🟡", "unsupported": "❌"}
return symbols.get(status, "❓")
def generate_markdown(self) -> str:
lines = []
lines.append("# GGML Operations")
lines.append("")
lines.append("List of GGML operations and backend support status.")
lines.append("")
lines.append("## How to add a backend to this table:")
lines.append("")
lines.append("1. Run `test-backend-ops support --output csv` with your backend name and redirect output to a csv file in `docs/ops/` (e.g., `docs/ops/CUDA.csv`)")
lines.append("2. Regenerate `/docs/ops.md` via `./scripts/create_ops_docs.py`")
lines.append("")
lines.append("Legend:")
lines.append("- ✅ Fully supported by this backend")
lines.append("- 🟡 Partially supported by this backend")
lines.append("- ❌ Not supported by this backend")
lines.append("")
backends = sorted(self.all_backends)
header = "| Operation |"
for backend in backends:
header += f" {backend} |"
separator = "|-----------|"
for _ in backends:
separator += "------|"
lines.append(header)
lines.append(separator)
sorted_operations = sorted(self.all_operations)
for operation in sorted_operations:
row = f"| {operation:>32} |"
for backend in backends:
status = self.get_backend_support_status(backend, operation)
if status == "supported":
symbol = "✅"
elif status == "partially supported":
symbol = "🟡"
else:
symbol = "❌"
row += f" {symbol} |"
lines.append(row)
lines.append("")
return "\n".join(lines)
def run(self) -> None:
self.logger.info("Parsing GGML operation support files...")
self.parse_support_files()
if not self.all_operations:
self.logger.error(
"No operations found. Make sure to run test-backend-ops support --output csv > docs/ops/file.csv first."
)
return
self.logger.info(
f"Found {len(self.all_operations)} operations across {len(self.all_backends)} backends"
)
self.logger.info("Generating markdown...")
markdown_content = self.generate_markdown()
docs_dir = self.ggml_root / "docs"
docs_dir.mkdir(exist_ok=True)
ops_file = docs_dir / self.output_filename
with open(ops_file, "w") as f:
f.write(markdown_content)
self.logger.info(f"Generated: {ops_file}")
self.logger.info(f"Operations: {len(self.all_operations)}")
self.logger.info(f"Backends: {len(self.all_backends)}")
def main():
logging.basicConfig(level=logging.INFO)
if len(sys.argv) > 1:
output_filename = sys.argv[1]
else:
output_filename = "ops.md"
generator = DocsGenerator(".", output_filename)
generator.run()
if __name__ == "__main__":
main()
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
PROG=${0##*/}
build_dir="build-ci-debug"
# Print Color Commands
red=$(tput setaf 1)
green=$(tput setaf 2)
yellow=$(tput setaf 3)
blue=$(tput setaf 4)
magenta=$(tput setaf 5)
cyan=$(tput setaf 6)
normal=$(tput sgr0)
# Print Help Message
####################
print_full_help() {
cat << EOF
Usage: $PROG [OPTION]... <test_regex> (test_number)
Debug specific ctest program.
Options:
-h, --help display this help and exit
-g run in gdb mode
Arguments:
<test_regex> (Mandatory) Supply one regex to the script to filter tests
(test_number) (Optional) Test number to run a specific test
Example:
$PROG test-tokenizer
$PROG test-tokenizer 3
EOF
}
abort() {
echo "Error: $1" >&2
cat << EOF >&2
Usage: $PROG [OPTION]... <test_regex> (test_number)
Debug specific ctest program.
Refer to --help for full instructions.
EOF
exit 1
}
# Dependency Sanity Check
#########################
check_dependency() {
command -v "$1" >/dev/null 2>&1 || {
abort "$1 is required but not found. Please install it and try again."
}
}
check_dependency ctest
check_dependency cmake
# Step 0: Check the args
########################
if [ x"$1" = x"-h" ] || [ x"$1" = x"--help" ]; then
print_full_help >&2
exit 0
fi
# Parse command-line options
gdb_mode=false
while getopts "g" opt; do
case $opt in
g)
gdb_mode=true
echo "gdb_mode Mode Enabled"
;;
esac
done
# Shift the option parameters
shift $((OPTIND - 1))
# Positionial Argument Processing : <test_regex>
if [ -z "${1}" ]; then
abort "Test regex is required"
else
test_suite=${1:-}
fi
# Positionial Argument Processing : (test_number)
test_number=${2:-}
# Step 1: Reset and Setup folder context
########################################
## Sanity check that we are actually in a git repo
repo_root=$(git rev-parse --show-toplevel)
if [ ! -d "$repo_root" ]; then
abort "Not in a Git repository."
fi
## Reset folder to root context of git repo and Create and enter build directory
pushd "$repo_root"
rm -rf "$build_dir" && mkdir "$build_dir" || abort "Failed to make $build_dir"
# Step 2: Setup Build Environment and Compile Test Binaries
###########################################################
cmake -B "./$build_dir" -DCMAKE_BUILD_TYPE=Debug -DGGML_CUDA=1 || abort "Failed to build environment"
pushd "$build_dir"
make -j || abort "Failed to compile"
popd > /dev/null || exit 1
# Step 3: Find all tests available that matches REGEX
####################################################
# Ctest Gather Tests
# `-R test-tokenizer` : looks for all the test files named `test-tokenizer*` (R=Regex)
# `-N` : "show-only" disables test execution & shows test commands that you can feed to GDB.
# `-V` : Verbose Mode
printf "\n\nGathering tests that fit REGEX: ${test_suite} ...\n"
pushd "$build_dir"
tests=($(ctest -R ${test_suite} -V -N | grep -E " +Test +#[0-9]+*" | cut -d':' -f2 | awk '{$1=$1};1'))
if [ ${#tests[@]} -eq 0 ]; then
abort "No tests available... check your compilation process..."
fi
popd > /dev/null || exit 1
# Step 4: Identify Test Command for Debugging
#############################################
# Select test number
if [ -z $test_number ]; then
# List out available tests
printf "Which test would you like to debug?\n"
id=0
for s in "${tests[@]}"
do
echo "Test# ${id}"
echo " $s"
((id++))
done
# Prompt user which test they wanted to run
printf "\nRun test#? "
read test_number
else
printf "\nUser Already Requested #${test_number}\n"
fi
# Grab all tests commands
pushd "$build_dir"
sIFS=$IFS # Save Initial IFS (Internal Field Separator)
IFS=$'\n' # Change IFS (Internal Field Separator) (So we split ctest output by newline rather than by spaces)
test_args=($(ctest -R ${test_suite} -V -N | grep "Test command" | cut -d':' -f3 | awk '{$1=$1};1' )) # Get test args
IFS=$sIFS # Reset IFS (Internal Field Separator)
popd > /dev/null || exit 1
# Grab specific test command
single_test_name="${tests[test_number]}"
single_test_command="${test_args[test_number]}"
# Step 5: Execute or GDB Debug
##############################
printf "${magenta}Running Test #${test_number}: ${single_test_name}${normal}\n"
printf "${cyan}single_test_command: ${single_test_command}${normal}\n"
if [ "$gdb_mode" = "true" ]; then
# Execute debugger
pushd "$repo_root" || exit 1
eval "gdb --args ${single_test_command}"
popd > /dev/null || exit 1
else
# Execute Test
pushd "$repo_root" || exit 1
eval "${single_test_command}"
exit_code=$?
popd > /dev/null || exit 1
# Print Result
printf "${blue}Ran Test #${test_number}: ${single_test_name}${normal}\n"
printf "${yellow}Command: ${single_test_command}${normal}\n"
if [ $exit_code -eq 0 ]; then
printf "${green}TEST PASS${normal}\n"
else
printf "${red}TEST FAIL${normal}\n"
fi
fi
# Return to the directory from which the user ran the command.
popd > /dev/null || exit 1
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
printf "# date: $(date)\n" > AUTHORS
printf "# this file is auto-generated by scripts/gen-authors.sh\n\n" >> AUTHORS
git log --format='%an <%ae>' --reverse --date=short master | awk '!seen[$0]++' | sort >> AUTHORS
# if necessary, update your name here. for example: jdoe -> John Doe
sed -i '' 's/^jdoe/John Doe/g' AUTHORS
+234
View File
@@ -0,0 +1,234 @@
from __future__ import annotations
import array
import sys
import unicodedata
import requests
def out(line=""):
print(line, end='\n') # noqa
def normalization_tables():
out(f"// Canonical normalization tables: Unicode {unicodedata.unidata_version}.")
out("const std::unordered_map<uint32_t, uint8_t> unicode_combining_class = {")
for cpt in range(0x110000):
if value := unicodedata.combining(chr(cpt)):
out(f"{{0x{cpt:06X}, {value}}},")
out("};\n")
out("const std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> unicode_decomposition = {")
compositions = []
for cpt in range(0x110000):
if 0xAC00 <= cpt < 0xAC00 + 11172:
continue
raw = unicodedata.decomposition(chr(cpt))
if not raw or raw.startswith("<"):
continue
parts = [int(part, 16) for part in raw.split()]
assert 1 <= len(parts) <= 2
first, second = parts[0], parts[1] if len(parts) == 2 else 0
out(f"{{0x{cpt:06X}, {{0x{first:06X}, 0x{second:06X}}}}},")
if second and unicodedata.normalize("NFC", chr(first) + chr(second)) == chr(cpt):
compositions.append((first, second, cpt))
out("};\n")
out("const std::unordered_map<uint64_t, uint32_t> unicode_composition = {")
for first, second, cpt in compositions:
out(f"{{0x{((first << 32) | second):016X}ULL, 0x{cpt:06X}}},")
out("};")
# Generate only canonical tables, without downloading the UCD data.
if sys.argv[1:] == ["--normalization-only"]:
normalization_tables()
sys.exit(0)
MAX_CODEPOINTS = 0x110000
UNICODE_DATA_URL = "https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt"
# see https://www.unicode.org/L2/L1999/UnicodeData.html
def unicode_data_iter():
res = requests.get(UNICODE_DATA_URL)
res.raise_for_status()
data = res.content.decode()
prev = []
for line in data.splitlines():
# ej: 0000;<control>;Cc;0;BN;;;;;N;NULL;;;;
line = line.split(";")
cpt = int(line[0], base=16)
assert cpt < MAX_CODEPOINTS
cpt_lower = int(line[-2] or "0", base=16)
assert cpt_lower < MAX_CODEPOINTS
cpt_upper = int(line[-3] or "0", base=16)
assert cpt_upper < MAX_CODEPOINTS
categ = line[2].strip()
assert len(categ) == 2
bidir = line[4].strip()
assert len(categ) == 2
name = line[1]
if name.endswith(", First>"):
prev = (cpt, cpt_lower, cpt_upper, categ, bidir)
continue
if name.endswith(", Last>"):
assert prev[1:] == (0, 0, categ, bidir)
for c in range(prev[0], cpt):
yield (c, cpt_lower, cpt_upper, categ, bidir)
yield (cpt, cpt_lower, cpt_upper, categ, bidir)
# see definition in unicode.h
CODEPOINT_FLAG_UNDEFINED = 0x0001 #
CODEPOINT_FLAG_NUMBER = 0x0002 # \p{N}
CODEPOINT_FLAG_LETTER = 0x0004 # \p{L}
CODEPOINT_FLAG_SEPARATOR = 0x0008 # \p{Z}
CODEPOINT_FLAG_MARK = 0x0010 # \p{M}
CODEPOINT_FLAG_PUNCTUATION = 0x0020 # \p{P}
CODEPOINT_FLAG_SYMBOL = 0x0040 # \p{S}
CODEPOINT_FLAG_CONTROL = 0x0080 # \p{C}
UNICODE_CATEGORY_TO_FLAG = {
"Cn": CODEPOINT_FLAG_UNDEFINED, # Undefined
"Cc": CODEPOINT_FLAG_CONTROL, # Control
"Cf": CODEPOINT_FLAG_CONTROL, # Format
"Co": CODEPOINT_FLAG_CONTROL, # Private Use
"Cs": CODEPOINT_FLAG_CONTROL, # Surrrogate
"Ll": CODEPOINT_FLAG_LETTER, # Lowercase Letter
"Lm": CODEPOINT_FLAG_LETTER, # Modifier Letter
"Lo": CODEPOINT_FLAG_LETTER, # Other Letter
"Lt": CODEPOINT_FLAG_LETTER, # Titlecase Letter
"Lu": CODEPOINT_FLAG_LETTER, # Uppercase Letter
"L&": CODEPOINT_FLAG_LETTER, # Cased Letter
"Mc": CODEPOINT_FLAG_MARK, # Spacing Mark
"Me": CODEPOINT_FLAG_MARK, # Enclosing Mark
"Mn": CODEPOINT_FLAG_MARK, # Nonspacing Mark
"Nd": CODEPOINT_FLAG_NUMBER, # Decimal Number
"Nl": CODEPOINT_FLAG_NUMBER, # Letter Number
"No": CODEPOINT_FLAG_NUMBER, # Other Number
"Pc": CODEPOINT_FLAG_PUNCTUATION, # Connector Punctuation
"Pd": CODEPOINT_FLAG_PUNCTUATION, # Dash Punctuation
"Pe": CODEPOINT_FLAG_PUNCTUATION, # Close Punctuation
"Pf": CODEPOINT_FLAG_PUNCTUATION, # Final Punctuation
"Pi": CODEPOINT_FLAG_PUNCTUATION, # Initial Punctuation
"Po": CODEPOINT_FLAG_PUNCTUATION, # Other Punctuation
"Ps": CODEPOINT_FLAG_PUNCTUATION, # Open Punctuation
"Sc": CODEPOINT_FLAG_SYMBOL, # Currency Symbol
"Sk": CODEPOINT_FLAG_SYMBOL, # Modifier Symbol
"Sm": CODEPOINT_FLAG_SYMBOL, # Math Symbol
"So": CODEPOINT_FLAG_SYMBOL, # Other Symbol
"Zl": CODEPOINT_FLAG_SEPARATOR, # Line Separator
"Zp": CODEPOINT_FLAG_SEPARATOR, # Paragraph Separator
"Zs": CODEPOINT_FLAG_SEPARATOR, # Space Separator
}
codepoint_flags = array.array('H', [CODEPOINT_FLAG_UNDEFINED]) * MAX_CODEPOINTS
table_whitespace = []
table_lowercase = []
table_uppercase = []
table_nfd = []
for (cpt, cpt_lower, cpt_upper, categ, bidir) in unicode_data_iter():
# convert codepoint to unicode character
char = chr(cpt)
# codepoint category flags
codepoint_flags[cpt] = UNICODE_CATEGORY_TO_FLAG[categ]
# lowercase conversion
if cpt_lower:
table_lowercase.append((cpt, cpt_lower))
# uppercase conversion
if cpt_upper:
table_uppercase.append((cpt, cpt_upper))
# NFD normalization
norm = ord(unicodedata.normalize('NFD', char)[0])
if cpt != norm:
table_nfd.append((cpt, norm))
# whitespaces, see "<White_Space>" https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt
table_whitespace.extend(range(0x0009, 0x000D + 1))
table_whitespace.extend(range(0x2000, 0x200A + 1))
table_whitespace.extend([0x0020, 0x0085, 0x00A0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000])
# sort by codepoint
table_whitespace.sort()
table_lowercase.sort()
table_uppercase.sort()
table_nfd.sort()
# group ranges with same flags
ranges_flags: list[tuple[int, int]] = [(0, codepoint_flags[0])] # start, flags
for codepoint, flags in enumerate(codepoint_flags):
if flags != ranges_flags[-1][1]:
ranges_flags.append((codepoint, flags))
ranges_flags.append((MAX_CODEPOINTS, 0x0000))
# group ranges with same nfd
ranges_nfd: list[tuple[int, int, int]] = [(0, 0, 0)] # start, last, nfd
for codepoint, norm in table_nfd:
start = ranges_nfd[-1][0]
if ranges_nfd[-1] != (start, codepoint - 1, norm):
ranges_nfd.append((0, 0, 0)) # dummy, will be replaced below
start = codepoint
ranges_nfd[-1] = (start, codepoint, norm)
# Generate 'unicode-data.cpp':
# python ./scripts//gen-unicode-data.py > unicode-data.cpp
out("""\
// generated with scripts/gen-unicode-data.py
#include "unicode-data.h"
#include <cstdint>
#include <vector>
#include <unordered_map>
#include <unordered_set>
""")
out("const std::vector<std::pair<uint32_t, uint16_t>> unicode_ranges_flags = { // start, flags // last=next_start-1")
for codepoint, flags in ranges_flags:
out("{0x%06X, 0x%04X}," % (codepoint, flags))
out("};\n")
out("const std::unordered_set<uint32_t> unicode_set_whitespace = {")
for codepoint in table_whitespace:
out("0x%06X," % codepoint)
out("};\n")
out("const std::unordered_map<uint32_t, uint32_t> unicode_map_lowercase = {")
for tuple_lw in table_lowercase:
out("{0x%06X, 0x%06X}," % tuple_lw)
out("};\n")
out("const std::unordered_map<uint32_t, uint32_t> unicode_map_uppercase = {")
for tuple_up in table_uppercase:
out("{0x%06X, 0x%06X}," % tuple_up)
out("};\n")
out("const std::vector<range_nfd> unicode_ranges_nfd = { // start, last, nfd")
for triple in ranges_nfd:
out("{0x%06X, 0x%06X, 0x%06X}," % triple)
out("};\n")
normalization_tables()
+38
View File
@@ -0,0 +1,38 @@
ifeq '' '$(findstring clang,$(shell $(GF_CC) --version))'
GF_CC_IS_GCC = 1
GF_CC_VER := $(shell { $(GF_CC) -dumpfullversion 2>/dev/null; echo; $(GF_CC) -dumpversion; } | awk -F. '/./ { printf("%02d%02d%02d", $$1, $$2, $$3); exit }')
else
GF_CC_IS_CLANG = 1
ifeq '' '$(findstring Apple,$(shell $(GF_CC) --version))'
GF_CC_IS_LLVM_CLANG = 1
else
GF_CC_IS_APPLE_CLANG = 1
endif
GF_CC_VER := \
$(shell $(GF_CC) --version | sed -n 's/^.* version \([0-9.]*\).*$$/\1/p' \
| awk -F. '{ printf("%02d%02d%02d", $$1, $$2, $$3) }')
endif
ifeq ($(GF_CC_IS_CLANG), 1)
# clang options
GF_CFLAGS = -Wunreachable-code-break -Wunreachable-code-return
GF_CXXFLAGS = -Wunreachable-code-break -Wunreachable-code-return -Wmissing-prototypes -Wextra-semi
ifneq '' '$(and $(GF_CC_IS_LLVM_CLANG),$(filter 1,$(shell expr $(GF_CC_VER) \>= 030800)))'
GF_CFLAGS += -Wdouble-promotion
endif
ifneq '' '$(and $(GF_CC_IS_APPLE_CLANG),$(filter 1,$(shell expr $(GF_CC_VER) \>= 070300)))'
GF_CFLAGS += -Wdouble-promotion
endif
else
# gcc options
GF_CFLAGS = -Wdouble-promotion
GF_CXXFLAGS = -Wno-array-bounds
ifeq ($(shell expr $(GF_CC_VER) \>= 070100), 1)
GF_CXXFLAGS += -Wno-format-truncation
endif
ifeq ($(shell expr $(GF_CC_VER) \>= 080100), 1)
GF_CXXFLAGS += -Wextra-semi
endif
endif
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# vim: set ts=4 sw=4 et:
FILE="hellaswag_val_full.txt"
URL="https://raw.githubusercontent.com/klosax/hellaswag_text_data/main/$FILE"
die() {
printf "%s\n" "$@" >&2
exit 1
}
have_cmd() {
for cmd; do
command -v "$cmd" >/dev/null || return
done
}
dl() {
[ -f "$2" ] && return
if have_cmd wget; then
wget "$1" -O "$2"
elif have_cmd curl; then
curl -L "$1" -o "$2"
else
die "Please install wget or curl"
fi
}
if [ ! -f "$FILE" ]; then
dl "$URL" "$FILE" || exit
fi
cat <<EOF
Usage:
llama-perplexity -m model.gguf -f $FILE --hellaswag [--hellaswag-tasks N] [other params]
EOF
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
function usage {
echo "usage: <n>$0"
echo "note: n is the number of essays to download"
echo "for specific n, the resulting pg.txt file will have the following number of tokens:"
echo "n | tokens"
echo "--- | ---"
echo "1 | 6230"
echo "2 | 23619"
echo "5 | 25859"
echo "10 | 36888"
echo "15 | 50188"
echo "20 | 59094"
echo "25 | 88764"
echo "30 | 103121"
echo "32 | 108338"
echo "35 | 113403"
echo "40 | 127699"
echo "45 | 135896"
exit 1
}
function has_cmd {
if ! [ -x "$(command -v $1)" ]; then
echo "error: $1 is not available" >&2
exit 1
fi
}
# check for: curl, html2text, tail, sed, fmt
has_cmd curl
has_cmd html2text
has_cmd tail
has_cmd sed
if [ $# -ne 1 ]; then
usage
fi
n=$1
# get urls
urls="$(curl http://www.aaronsw.com/2002/feeds/pgessays.rss | grep html | sed -e "s/.*http/http/" | sed -e "s/html.*/html/" | head -n $n)"
printf "urls:\n%s\n" "$urls"
if [ -f pg.txt ]; then
rm pg.txt
fi
c=1
for url in $urls; do
echo "processing $url"
cc=$(printf "%03d" $c)
curl -L $url | html2text | tail -n +4 | sed -E "s/^[[:space:]]+//g" | fmt -w 80 >> pg-$cc-one.txt
cat pg-$cc-one.txt >> pg.txt
cp -v pg.txt pg-$cc-all.txt
c=$((c+1))
# don't flood the server
sleep 1
done
echo "done. data in pg.txt"
exit 0
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh
# vim: set ts=4 sw=4 et:
ZIP="wikitext-2-raw-v1.zip"
FILE="wikitext-2-raw/wiki.test.raw"
URL="https://huggingface.co/datasets/ggml-org/ci/resolve/main/$ZIP"
die() {
printf "%s\n" "$@" >&2
exit 1
}
have_cmd() {
for cmd; do
command -v "$cmd" >/dev/null || return
done
}
dl() {
[ -f "$2" ] && return
if have_cmd wget; then
wget "$1" -O "$2"
elif have_cmd curl; then
curl -L "$1" -o "$2"
else
die "Please install wget or curl"
fi
}
have_cmd unzip || die "Please install unzip"
if [ ! -f "$FILE" ]; then
dl "$URL" "$ZIP" || exit
unzip -o "$ZIP" || exit
rm -f -- "$ZIP"
fi
cat <<EOF
Usage:
llama-perplexity -m model.gguf -f $FILE [other params]
EOF
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# vim: set ts=4 sw=4 et:
FILE="winogrande-debiased-eval.csv"
URL="https://huggingface.co/datasets/ikawrakow/winogrande-eval-for-llama.cpp/raw/main/$FILE"
die() {
printf "%s\n" "$@" >&2
exit 1
}
have_cmd() {
for cmd; do
command -v "$cmd" >/dev/null || return
done
}
dl() {
[ -f "$2" ] && return
if have_cmd wget; then
wget "$1" -O "$2"
elif have_cmd curl; then
curl -L "$1" -o "$2"
else
die "Please install wget or curl"
fi
}
if [ ! -f "$FILE" ]; then
dl "$URL" "$FILE" || exit
fi
cat <<EOF
Usage:
llama-perplexity -m model.gguf -f $FILE --winogrande [--winogrande-tasks N] [other params]
EOF
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python
'''
Fetches the Jinja chat template of a HuggingFace model.
If a model has multiple chat templates, you can specify the variant name.
Syntax:
./scripts/get_chat_template.py model_id [variant]
Examples:
./scripts/get_chat_template.py CohereForAI/c4ai-command-r-plus tool_use
./scripts/get_chat_template.py microsoft/Phi-3.5-mini-instruct
'''
import json
import re
import sys
def get_chat_template(model_id, variant=None):
try:
# Use huggingface_hub library if available.
# Allows access to gated models if the user has access and ran `huggingface-cli login`.
from huggingface_hub import hf_hub_download
with open(hf_hub_download(repo_id=model_id, filename="tokenizer_config.json"), encoding="utf-8") as f:
config_str = f.read()
except ImportError:
import requests
assert re.match(r"^[\w.-]+/[\w.-]+$", model_id), f"Invalid model ID: {model_id}"
response = requests.get(f"https://huggingface.co/{model_id}/resolve/main/tokenizer_config.json")
if response.status_code == 401:
raise Exception('Access to this model is gated, please request access, authenticate with `huggingface-cli login` and make sure to run `pip install huggingface_hub`')
response.raise_for_status()
config_str = response.text
try:
config = json.loads(config_str)
except json.JSONDecodeError:
# Fix https://huggingface.co/NousResearch/Meta-Llama-3-8B-Instruct/blob/main/tokenizer_config.json
# (Remove extra '}' near the end of the file)
config = json.loads(re.sub(r'\}([\n\s]*\}[\n\s]*\],[\n\s]*"clean_up_tokenization_spaces")', r'\1', config_str))
chat_template = config['chat_template']
if isinstance(chat_template, str):
return chat_template
else:
variants = {
ct['name']: ct['template']
for ct in chat_template
}
def format_variants():
return ', '.join(f'"{v}"' for v in variants.keys())
if variant is None:
if 'default' not in variants:
raise Exception(f'Please specify a chat template variant (one of {format_variants()})')
variant = 'default'
sys.stderr.write(f'Note: picked "default" chat template variant (out of {format_variants()})\n')
elif variant not in variants:
raise Exception(f"Variant {variant} not found in chat template (found {format_variants()})")
return variants[variant]
def main(args):
if len(args) < 1:
raise ValueError("Please provide a model ID and an optional variant name")
model_id = args[0]
variant = None if len(args) < 2 else args[1]
template = get_chat_template(model_id, variant)
sys.stdout.write(template)
if __name__ == '__main__':
main(sys.argv[1:])
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
cmake_args=()
llama_results_args=()
for arg in "${@}"; do
if [[ "$arg" == -D* ]]; then
cmake_args+=("$arg")
else
llama_results_args+=("$arg")
fi
done
dir="build-bisect"
rm -rf ${dir} > /dev/null
cmake -B ${dir} -S . ${cmake_args} > /dev/null
cmake --build ${dir} -t llama-results -j $(nproc) > /dev/null
${dir}/bin/llama-results "${llama_results_args[@]}"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
if [ $# -lt 2 ]; then
echo "usage: ./scripts/git-bisect.sh <commit_bad> <commit_good> [additional arguments]"
echo " additional arguments: passed to CMake if they start with \"-D\", to llama-results otherwise"
exit 1
fi
set -e
set -x
commit_bad=$1
commit_good=$2
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
git checkout ${commit_good}
${script_dir}/git-bisect-run.sh --output results.gguf "${@:3}"
git bisect start ${commit_bad} ${commit_good}
git bisect run ${script_dir}/git-bisect-run.sh --output results.gguf --check "${@:3}"
git bisect reset
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
#
# Shortcut for downloading HF models
#
# Usage:
# ./llama-cli -m $(./scripts/hf.sh https://huggingface.co/TheBloke/Mixtral-8x7B-v0.1-GGUF/resolve/main/mixtral-8x7b-v0.1.Q4_K_M.gguf)
# ./llama-cli -m $(./scripts/hf.sh --url https://huggingface.co/TheBloke/Mixtral-8x7B-v0.1-GGUF/blob/main/mixtral-8x7b-v0.1.Q4_K_M.gguf)
# ./llama-cli -m $(./scripts/hf.sh --repo TheBloke/Mixtral-8x7B-v0.1-GGUF --file mixtral-8x7b-v0.1.Q4_K_M.gguf)
#
# all logs go to stderr
function log {
echo "$@" 1>&2
}
function usage {
log "Usage: $0 [[--url] <url>] [--repo <repo>] [--file <file>] [--outdir <dir> [-h|--help]"
exit 1
}
# check for curl or wget
function has_cmd {
if ! [ -x "$(command -v $1)" ]; then
return 1
fi
}
if has_cmd wget; then
cmd="wget -q -c -O %s/%s %s"
elif has_cmd curl; then
cmd="curl -C - -f --output-dir %s -o %s -L %s"
else
log "[E] curl or wget not found"
exit 1
fi
url=""
repo=""
file=""
outdir="."
# parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--url)
url="$2"
shift 2
;;
--repo)
repo="$2"
shift 2
;;
--file)
file="$2"
shift 2
;;
--outdir)
outdir="$2"
shift 2
;;
-h|--help)
usage
;;
*)
url="$1"
shift
;;
esac
done
if [ -n "$repo" ] && [ -n "$file" ]; then
url="https://huggingface.co/$repo/resolve/main/$file"
fi
if [ -z "$url" ]; then
log "[E] missing --url"
usage
fi
# check if the URL is a HuggingFace model, and if so, try to download it
is_url=false
if [[ ${#url} -gt 22 ]]; then
if [[ ${url:0:22} == "https://huggingface.co" ]]; then
is_url=true
fi
fi
if [ "$is_url" = false ]; then
log "[E] invalid URL, must start with https://huggingface.co"
exit 0
fi
# replace "blob/main" with "resolve/main"
url=${url/blob\/main/resolve\/main}
basename=$(basename $url)
log "[+] attempting to download $basename"
if [ -n "$cmd" ]; then
cmd=$(printf "$cmd" "$outdir" "$basename" "$url")
log "[+] $cmd"
if $cmd; then
echo $outdir/$basename
exit 0
fi
fi
log "[-] failed to download"
exit 1
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
import sys
from collections import defaultdict
import re
def parse_log_file(filepath):
functions = defaultdict(lambda: {'vgprs': 0, 'spill': 0, 'location': ''})
func_stack = []
try:
with open(filepath, 'r') as f:
for line in f:
# Match function name lines
func_match = re.search(r'remark: ([^:]+):(\d+):\d+: Function Name: (\S+)', line)
if func_match:
location = func_match.group(1) + ':' + func_match.group(2)
func_name = func_match.group(3)
# Extract just the filename and line number
parts = location.split('/')
short_location = parts[-1] if len(parts) > 0 else location
functions[func_name]['location'] = short_location
# Push function onto stack with its location
func_stack.append({'name': func_name, 'location': location})
continue
# Match VGPR usage lines (only if we have functions in stack)
vgpr_match = re.search(r'remark: ([^:]+):(\d+):\d+:\s+VGPRs: (\d+)', line)
if vgpr_match:
location = vgpr_match.group(1) + ':' + vgpr_match.group(2)
# Find the most recent function with matching location
for i in range(len(func_stack) - 1, -1, -1):
if func_stack[i]['location'] == location:
functions[func_stack[i]['name']]['vgprs'] = int(vgpr_match.group(3))
break
continue
spill_match = re.search(r'remark: ([^:]+):(\d+):\d+:\s+VGPRs Spill: (\d+)', line)
if spill_match:
location = spill_match.group(1) + ':' + spill_match.group(2)
# Find the most recent function with matching location
for i in range(len(func_stack) - 1, -1, -1):
if func_stack[i]['location'] == location:
functions[func_stack[i]['name']]['spill'] = int(spill_match.group(3))
break
continue
except FileNotFoundError:
print(f"Error: File {filepath} not found", file=sys.stderr) # noqa: NP100
sys.exit(1)
return functions
def main():
if len(sys.argv) < 2:
print("Usage: ./vgpr_check.py <log_file>", file=sys.stderr) # noqa: NP100
sys.exit(1)
log_file = sys.argv[1]
ignored = {
'_ZL21gated_linear_attn_f32ILi128EEviiiifPKfS1_S1_S1_S1_Pf',
'_ZL13rwkv_wkv7_f32ILi128EEviiiiPKfS1_S1_S1_S1_S1_S1_Pf',
'_ZL12rwkv_wkv_f32ILi128EEviiiiPKfS1_S1_S1_S1_S1_Pf',
'_ZL9mul_mat_qIL9ggml_type10ELi64ELb1EEvPKcPKiS4_S4_PfS5_PKf15HIP_vector_typeIjLj3EEiiiiiS9_S9_iiiS9_S9_iiiS9_',
'_ZL9mul_mat_qIL9ggml_type42ELi128ELb1EEvPKcPKiS4_S4_PfS5_PKf15HIP_vector_typeIjLj3EEiiiiiS9_S9_iiiS9_S9_iiiS9_',
'_ZL18flash_attn_ext_f16ILi576ELi512ELi2ELi32ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi16ELi4ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi16ELi4ELb1ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi32ELi2ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi32ELi2ELb1ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi8ELi8ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi8ELi8ELb1ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi576ELi512ELi16ELi4ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi576ELi512ELi4ELi16ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi16ELi2ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi16ELi2ELb1ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi4ELi8ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi4ELi8ELb1ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi8ELi4ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi512ELi512ELi8ELi4ELb1ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi576ELi512ELi1ELi32ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi576ELi512ELi2ELi16ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi576ELi512ELi8ELi4ELb0ELb1ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_vecILi256ELi1EL9ggml_type2ELS0_2ELb0EEvPKcS2_S2_S2_S2_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS6_IjLj3EEiiiiiiiiiiiliiliiiiil',
'_ZL18flash_attn_ext_f16ILi320ELi256ELi2ELi32ELb0ELb0ELb0EEvPKcS1_S1_S1_S1_PKiPfP15HIP_vector_typeIfLj2EEffffjfiS5_IjLj3EEiiiiiiiiiiiliiliiiiil',
}
functions = parse_log_file(log_file)
found_issues = False
# First print all ignored functions (deduplicated)
printed_ignored = set()
for func_name, data in sorted(functions.items()):
total_vgprs = int(data['vgprs']) + int(data['spill'])
if total_vgprs > 256 and func_name in ignored and func_name not in printed_ignored:
location = data.get('location', log_file)
print(f"{location}: {func_name} - Total VGPRs: {total_vgprs} ({data['vgprs']} + {data['spill']}) [IGNORED]") # noqa: NP100
printed_ignored.add(func_name)
# Then print new functions with issues in red
for func_name, data in sorted(functions.items()):
total_vgprs = int(data['vgprs']) + int(data['spill'])
if total_vgprs > 256 and func_name not in ignored:
status = "[IGNORED]" if func_name in ignored else ""
location = data.get('location', log_file)
# Print in red if not ignored
color_code = "\033[91m" if func_name not in ignored else ""
reset_code = "\033[0m" if func_name not in ignored else ""
print(f"{color_code}{location}: {func_name} - Total VGPRs: {total_vgprs} ({data['vgprs']} + {data['spill']}) {status}{reset_code}") # noqa: NP100
if func_name not in ignored:
found_issues = True
sys.exit(1 if found_issues else 0)
if __name__ == "__main__":
main()
+19
View File
@@ -0,0 +1,19 @@
:: MIT license
:: Copyright (C) 2024 Intel Corporation
:: SPDX-License-Identifier: MIT
set URL=%1
set COMPONENTS=%2
curl.exe --output %TEMP%\webimage.exe --url %URL% --retry 5 --retry-delay 5
start /b /wait %TEMP%\webimage.exe -s -x -f webimage_extracted --log extract.log
del %TEMP%\webimage.exe
if "%COMPONENTS%"=="" (
webimage_extracted\bootstrapper.exe -s --action install --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 -p=NEED_VS2022_INTEGRATION=0 --log-dir=.
) else (
webimage_extracted\bootstrapper.exe -s --action install --components=%COMPONENTS% --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 -p=NEED_VS2022_INTEGRATION=0 --log-dir=.
)
set installer_exit_code=%ERRORLEVEL%
rd /s/q "webimage_extracted"
exit /b %installer_exit_code%
+504
View File
@@ -0,0 +1,504 @@
#!/usr/bin/env python3
import sys
import json
import argparse
import jinja2.ext as jinja2_ext
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QTextEdit,
QPushButton,
QFileDialog,
)
from PySide6.QtGui import QColor, QColorConstants, QTextCursor, QTextFormat
from PySide6.QtCore import Qt, QRect, QSize
from jinja2 import TemplateSyntaxError
from jinja2.sandbox import ImmutableSandboxedEnvironment
from datetime import datetime
def format_template_content(template_content):
"""Format the Jinja template content using Jinja2's lexer."""
if not template_content.strip():
return template_content
env = ImmutableSandboxedEnvironment()
tc_rstrip = template_content.rstrip()
tokens = list(env.lex(tc_rstrip))
result = ""
indent_level = 0
i = 0
while i < len(tokens):
token = tokens[i]
_, token_type, token_value = token
if token_type == "block_begin":
block_start = i
# Collect all tokens for this block construct
construct_content = token_value
end_token_type = token_type.replace("_begin", "_end")
j = i + 1
while j < len(tokens) and tokens[j][1] != end_token_type:
construct_content += tokens[j][2]
j += 1
if j < len(tokens): # Found the end token
construct_content += tokens[j][2]
i = j # Skip to the end token
# Check for control structure keywords for indentation
stripped_content = construct_content.strip()
instr = block_start + 1
while tokens[instr][1] == "whitespace":
instr = instr + 1
instruction_token = tokens[instr][2]
start_control_tokens = ["if", "for", "macro", "call", "block"]
end_control_tokens = ["end" + t for t in start_control_tokens]
is_control_start = any(
instruction_token.startswith(kw) for kw in start_control_tokens
)
is_control_end = any(
instruction_token.startswith(kw) for kw in end_control_tokens
)
# Adjust indentation for control structures
# For control end blocks, decrease indent BEFORE adding the content
if is_control_end:
indent_level = max(0, indent_level - 1)
# Remove all previous whitespace before this block
result = result.rstrip()
# Add proper indent, but only if this is not the first token
added_newline = False
if result: # Only add newline and indent if there's already content
result += (
"\n" + " " * indent_level
) # Use 2 spaces per indent level
added_newline = True
else: # For the first token, don't add any indent
result += ""
# Add the block content
result += stripped_content
# Add '-' after '%' if it wasn't there and we added a newline or indent
if (
added_newline
and stripped_content.startswith("{%")
and not stripped_content.startswith("{%-")
):
# Add '-' at the beginning
result = (
result[: result.rfind("{%")]
+ "{%-"
+ result[result.rfind("{%") + 2 :]
)
if stripped_content.endswith("%}") and not stripped_content.endswith(
"-%}"
):
# Only add '-' if this is not the last token or if there's content after
if i + 1 < len(tokens) and tokens[i + 1][1] != "eof":
result = result[:-2] + "-%}"
# For control start blocks, increase indent AFTER adding the content
if is_control_start:
indent_level += 1
else:
# Malformed template, just add the token
result += token_value
elif token_type == "variable_begin":
# Collect all tokens for this variable construct
construct_content = token_value
end_token_type = token_type.replace("_begin", "_end")
j = i + 1
while j < len(tokens) and tokens[j][1] != end_token_type:
construct_content += tokens[j][2]
j += 1
if j < len(tokens): # Found the end token
construct_content += tokens[j][2]
i = j # Skip to the end token
# For variable constructs, leave them alone
# Do not add indent or whitespace before or after them
result += construct_content
else:
# Malformed template, just add the token
result += token_value
elif token_type == "data":
# Handle data (text between Jinja constructs)
# For data content, preserve it as is
result += token_value
else:
# Handle any other tokens
result += token_value
i += 1
# Clean up trailing newlines and spaces
result = result.rstrip()
# Copy the newline / space count from the original
if (trailing_length := len(template_content) - len(tc_rstrip)):
result += template_content[-trailing_length:]
return result
# ------------------------
# Line Number Widget
# ------------------------
class LineNumberArea(QWidget):
def __init__(self, editor):
super().__init__(editor)
self.code_editor = editor
def sizeHint(self):
return QSize(self.code_editor.line_number_area_width(), 0)
def paintEvent(self, event):
self.code_editor.line_number_area_paint_event(event)
class CodeEditor(QPlainTextEdit):
def __init__(self):
super().__init__()
self.line_number_area = LineNumberArea(self)
self.blockCountChanged.connect(self.update_line_number_area_width)
self.updateRequest.connect(self.update_line_number_area)
self.cursorPositionChanged.connect(self.highlight_current_line)
self.update_line_number_area_width(0)
self.highlight_current_line()
def line_number_area_width(self):
digits = len(str(self.blockCount()))
space = 3 + self.fontMetrics().horizontalAdvance("9") * digits
return space
def update_line_number_area_width(self, _):
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
def update_line_number_area(self, rect, dy):
if dy:
self.line_number_area.scroll(0, dy)
else:
self.line_number_area.update(
0, rect.y(), self.line_number_area.width(), rect.height()
)
if rect.contains(self.viewport().rect()):
self.update_line_number_area_width(0)
def resizeEvent(self, event):
super().resizeEvent(event)
cr = self.contentsRect()
self.line_number_area.setGeometry(
QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())
)
def line_number_area_paint_event(self, event):
from PySide6.QtGui import QPainter
painter = QPainter(self.line_number_area)
painter.fillRect(event.rect(), QColorConstants.LightGray)
block = self.firstVisibleBlock()
block_number = block.blockNumber()
top = int(
self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
)
bottom = top + int(self.blockBoundingRect(block).height())
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
number = str(block_number + 1)
painter.setPen(QColorConstants.Black)
painter.drawText(
0,
top,
self.line_number_area.width() - 2,
self.fontMetrics().height(),
Qt.AlignmentFlag.AlignRight,
number,
)
block = block.next()
top = bottom
bottom = top + int(self.blockBoundingRect(block).height())
block_number += 1
def highlight_current_line(self):
extra_selections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
line_color = QColorConstants.Yellow.lighter(160)
selection.format.setBackground(line_color) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
selection.cursor = self.textCursor() # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
selection.cursor.clearSelection() # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
extra_selections.append(selection)
self.setExtraSelections(extra_selections)
def highlight_position(self, lineno: int, col: int, color: QColor):
block = self.document().findBlockByLineNumber(lineno - 1)
if block.isValid():
cursor = QTextCursor(block)
text = block.text()
start = block.position() + max(0, col - 1)
cursor.setPosition(start)
if col <= len(text):
cursor.movePosition(
QTextCursor.MoveOperation.NextCharacter,
QTextCursor.MoveMode.KeepAnchor,
)
extra = QTextEdit.ExtraSelection()
extra.format.setBackground(color.lighter(160)) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
extra.cursor = cursor # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
self.setExtraSelections(self.extraSelections() + [extra])
def highlight_line(self, lineno: int, color: QColor):
block = self.document().findBlockByLineNumber(lineno - 1)
if block.isValid():
cursor = QTextCursor(block)
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
extra = QTextEdit.ExtraSelection()
extra.format.setBackground(color.lighter(160)) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
extra.cursor = cursor # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
self.setExtraSelections(self.extraSelections() + [extra])
def clear_highlighting(self):
self.highlight_current_line()
# ------------------------
# Main App
# ------------------------
class JinjaTester(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Jinja Template Tester")
self.resize(1200, 800)
central = QWidget()
main_layout = QVBoxLayout(central)
# -------- Top input area --------
input_layout = QHBoxLayout()
# Template editor with label
template_layout = QVBoxLayout()
template_label = QLabel("Jinja2 Template")
template_layout.addWidget(template_label)
self.template_edit = CodeEditor()
template_layout.addWidget(self.template_edit)
input_layout.addLayout(template_layout)
# JSON editor with label
json_layout = QVBoxLayout()
json_label = QLabel("Context (JSON)")
json_layout.addWidget(json_label)
self.json_edit = CodeEditor()
self.json_edit.setPlainText("""
{
"add_generation_prompt": true,
"bos_token": "",
"eos_token": "",
"messages": [
{
"role": "user",
"content": "What is the capital of Poland?"
}
]
}
""".strip())
json_layout.addWidget(self.json_edit)
input_layout.addLayout(json_layout)
main_layout.addLayout(input_layout)
# -------- Rendered output area --------
output_label = QLabel("Rendered Output")
main_layout.addWidget(output_label)
self.output_edit = QPlainTextEdit()
self.output_edit.setReadOnly(True)
main_layout.addWidget(self.output_edit)
# -------- Render button and status --------
btn_layout = QHBoxLayout()
# Load template button
self.load_btn = QPushButton("Load Template")
self.load_btn.clicked.connect(self.load_template)
btn_layout.addWidget(self.load_btn)
# Format template button
self.format_btn = QPushButton("Format")
self.format_btn.clicked.connect(self.format_template)
btn_layout.addWidget(self.format_btn)
self.render_btn = QPushButton("Render")
self.render_btn.clicked.connect(self.render_template)
btn_layout.addWidget(self.render_btn)
main_layout.addLayout(btn_layout)
# Status label below buttons
self.status_label = QLabel("Ready")
main_layout.addWidget(self.status_label)
self.setCentralWidget(central)
def render_template(self):
self.template_edit.clear_highlighting()
self.output_edit.clear()
template_str = self.template_edit.toPlainText()
json_str = self.json_edit.toPlainText()
# Parse JSON context
try:
context = json.loads(json_str) if json_str.strip() else {}
except Exception as e:
self.status_label.setText(f"❌ JSON Error: {e}")
return
def raise_exception(text: str) -> str:
raise RuntimeError(text)
env = ImmutableSandboxedEnvironment(
trim_blocks=True,
lstrip_blocks=True,
extensions=[jinja2_ext.loopcontrols],
)
env.filters["tojson"] = (
lambda x,
indent=None,
separators=None,
sort_keys=False,
ensure_ascii=False: json.dumps(
x,
indent=indent,
separators=separators,
sort_keys=sort_keys,
ensure_ascii=ensure_ascii,
)
)
env.globals["strftime_now"] = lambda format: datetime.now().strftime(format) # ty: ignore[invalid-assignment, invalid-argument-type]
env.globals["raise_exception"] = raise_exception # ty: ignore[invalid-assignment]
try:
template = env.from_string(template_str)
output = template.render(context)
self.output_edit.setPlainText(output)
self.status_label.setText("✅ Render successful")
except TemplateSyntaxError as e:
self.status_label.setText(f"❌ Syntax Error (line {e.lineno}): {e.message}")
if e.lineno:
self.template_edit.highlight_line(e.lineno, QColor("red"))
except Exception as e:
# Catch all runtime errors
# Try to extract template line number
lineno = None
tb = e.__traceback__
while tb:
frame = tb.tb_frame
if frame.f_code.co_filename == "<template>":
lineno = tb.tb_lineno
break
tb = tb.tb_next
error_msg = f"Runtime Error: {type(e).__name__}: {e}"
if lineno:
error_msg = f"Runtime Error at line {lineno} in template: {type(e).__name__}: {e}"
self.template_edit.highlight_line(lineno, QColor("orange"))
self.output_edit.setPlainText(error_msg)
self.status_label.setText(f"❌ {error_msg}")
def load_template(self):
"""Load a Jinja template from a file using a file dialog."""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Load Jinja Template",
"",
"Template Files (*.jinja *.j2 *.html *.txt);;All Files (*)",
)
if file_path:
try:
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()
self.template_edit.setPlainText(content)
self.status_label.setText(f"✅ Loaded template from {file_path}")
except Exception as e:
self.status_label.setText(f"❌ Error loading file: {str(e)}")
def format_template(self):
"""Format the Jinja template using Jinja2's lexer for proper parsing."""
try:
template_content = self.template_edit.toPlainText()
if not template_content.strip():
self.status_label.setText("⚠️ Template is empty")
return
formatted_content = format_template_content(template_content)
self.template_edit.setPlainText(formatted_content)
self.status_label.setText("✅ Template formatted")
except Exception as e:
self.status_label.setText(f"❌ Error formatting template: {str(e)}")
if __name__ == "__main__":
if len(sys.argv) > 1:
# CLI mode
parser = argparse.ArgumentParser(description="Jinja Template Tester")
parser.add_argument(
"--template", required=True, help="Path to Jinja template file"
)
parser.add_argument("--context", required=True, help="JSON string for context")
parser.add_argument(
"--action",
choices=["format", "render"],
default="render",
help="Action to perform",
)
args = parser.parse_args()
# Load template
with open(args.template, "r", encoding="utf-8") as f:
template_content = f.read()
# Load JSON
context = json.loads(args.context)
# Add missing variables
context.setdefault("bos_token", "")
context.setdefault("eos_token", "")
context.setdefault("add_generation_prompt", False)
env = ImmutableSandboxedEnvironment()
if args.action == "format":
formatted = format_template_content(template_content)
print(formatted) # noqa: NP100
elif args.action == "render":
template = env.from_string(template_content)
output = template.render(context)
print(output) # noqa: NP100
else:
# GUI mode
app = QApplication(sys.argv)
window = JinjaTester()
window.show()
sys.exit(app.exec())
+2
View File
@@ -0,0 +1,2 @@
PySide6
jinja2
+191
View File
@@ -0,0 +1,191 @@
#!/bin/bash
# Run all pre-release checks and determine the release version.
#
# Usage: make-release-checks.sh [--dry-run]
# --dry-run: warn on failures instead of aborting
#
# Env (when running in GitHub Actions):
# GH_TOKEN, GITHUB_REPOSITORY, GITHUB_OUTPUT
# RELEASE_BRANCH: when set, HEAD must belong to origin/RELEASE_BRANCH and must
# not be older than 3 days from the branch HEAD (skipped when unset)
# REQUIRE_DOCKER: set to "false" to skip the container image check and the
# release re-tagging of the nightly images in make-release.yml (default: true)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DRY_RUN=false
CHECKS_PASSED=true
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
*) echo "Unknown argument: $arg"; exit 1 ;;
esac
done
MAJOR=$(grep "set(LLAMA_VERSION_MAJOR" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*MAJOR \([0-9]*\).*/\1/')
MINOR=$(grep "set(LLAMA_VERSION_MINOR" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*MINOR \([0-9]*\).*/\1/')
PATCH=$(grep "set(LLAMA_VERSION_PATCH" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*PATCH \([0-9]*\).*/\1/')
VERSION="v${MAJOR}.${MINOR}.${PATCH}"
echo "Determined version: ${VERSION}"
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
fi
SHA=$(git rev-parse HEAD)
echo "Checking that commit ${SHA} belongs to the release branch..."
if [[ -z "${RELEASE_BRANCH:-}" ]]; then
echo "Warning: RELEASE_BRANCH not set - skipping commit check (local run)"
else
TIP="origin/${RELEASE_BRANCH}"
COMMIT_ERR=""
if ! git rev-parse --verify "${TIP}" >/dev/null 2>&1; then
COMMIT_ERR="branch ${RELEASE_BRANCH} not found on remote"
elif ! git merge-base --is-ancestor "${SHA}" "${TIP}"; then
COMMIT_ERR="commit ${SHA} is not part of branch ${RELEASE_BRANCH}"
else
COMMIT_TS=$(git show -s --format=%ct "${SHA}")
TIP_TS=$(git show -s --format=%ct "${TIP}")
AGE_DAYS=$(( (TIP_TS - COMMIT_TS) / 86400 ))
if (( TIP_TS - COMMIT_TS > 3 * 86400 )); then
COMMIT_ERR="commit ${SHA} is ${AGE_DAYS} day(s) older than the HEAD of ${RELEASE_BRANCH} (max: 3)"
fi
fi
if [[ -n "${COMMIT_ERR}" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo "Warning: ${COMMIT_ERR} (dry run, continuing)."
CHECKS_PASSED=false
else
echo "Error: ${COMMIT_ERR}"
exit 1
fi
else
echo "Commit ${SHA} is on branch ${RELEASE_BRANCH} and within 3 days of its HEAD - OK"
fi
fi
echo "Checking that tag ${VERSION} does not already exist..."
if git ls-remote --tags origin "${VERSION}" | grep -q "${VERSION}"; then
echo "Error: tag ${VERSION} already exists on remote"
exit 1
fi
echo "Tag ${VERSION} does not exist on remote - OK"
echo "Checking release.yml status for commit ${SHA}..."
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
echo "Warning: GITHUB_REPOSITORY not set - skipping CI check (local run)"
else
RUNS=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/release.yml/runs?per_page=100" \
--jq "[.workflow_runs[] | select(.head_sha == \"${SHA}\" and .conclusion == \"success\")] | length")
if [[ "$RUNS" -eq 0 ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo "Warning: no successful release.yml run found for HEAD (${SHA}) (dry run, continuing)."
CHECKS_PASSED=false
else
echo "Error: no successful release.yml run found for HEAD (${SHA})"
echo "The nightly build must complete successfully before making a release."
exit 1
fi
else
echo "Found successful release.yml run for HEAD."
fi
fi
MAJOR=$(grep "set(GGML_VERSION_MAJOR" "$REPO_ROOT/ggml/CMakeLists.txt" | sed 's/.*MAJOR \([0-9]*\).*/\1/')
MINOR=$(grep "set(GGML_VERSION_MINOR" "$REPO_ROOT/ggml/CMakeLists.txt" | sed 's/.*MINOR \([0-9]*\).*/\1/')
PATCH=$(grep "set(GGML_VERSION_PATCH" "$REPO_ROOT/ggml/CMakeLists.txt" | sed 's/.*PATCH \([0-9]*\).*/\1/')
GGML_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
echo "Local ggml version: ${GGML_VERSION}"
if ! git clone --depth 1 --branch "${GGML_VERSION}" https://github.com/ggml-org/ggml.git upstream-ggml 2>/dev/null; then
echo "Warning: tag ${GGML_VERSION} not found in upstream ggml - skipping comparison"
else
echo "Comparing local ggml/ src and include with upstream ${GGML_VERSION}..."
DIFF=$(diff -rq "$REPO_ROOT/ggml/src" upstream-ggml/src 2>&1 || true)
DIFF+=$(diff -rq "$REPO_ROOT/ggml/include" upstream-ggml/include 2>&1 || true)
DIFF+=$(diff "$REPO_ROOT/ggml/CMakeLists.txt" upstream-ggml/CMakeLists.txt 2>&1 || true)
rm -rf upstream-ggml
if [[ -n "$DIFF" ]]; then
echo "local ggml/ differs from upstream ${GGML_VERSION}:"
echo "$DIFF"
if [[ "$DRY_RUN" == "true" ]]; then
echo "Warning: would abort release due to ggml mismatch (dry run, continuing)."
CHECKS_PASSED=false
else
echo "Error: ggml must match upstream before making a release."
exit 1
fi
else
echo "local ggml/ matches upstream ${GGML_VERSION}"
fi
fi
echo "Checking container images for commit ${SHA}..."
NIGHTLY_TAG="$(git tag --points-at "${SHA}" | grep -E '(^|-)b[0-9]+(-[0-9a-f]{7})?$' | head -n 1 || true)"
if [[ -z "${NIGHTLY_TAG}" ]]; then
echo "Warning: no nightly tag points at ${SHA} - skipping container image check"
elif [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
echo "Warning: GITHUB_REPOSITORY not set - skipping container image check (local run)"
elif [[ "${REQUIRE_DOCKER:-true}" != "true" ]]; then
echo "Require Docker is disabled - skipping container image check and re-tagging"
else
CONTAINER_REPO="${GITHUB_REPOSITORY,,}" # lower-case owner/repo for ghcr.io
GHCR_TOKEN="$(curl -fsSL \
"https://ghcr.io/token?scope=repository:${CONTAINER_REPO}:pull&service=ghcr.io" \
| grep -oP '"token"\s*:\s*"\K[^"]+')"
VARIANTS=("" "-cuda" "-cuda13" "-vulkan" "-rocm" "-intel" "-musa" "-openvino")
TYPES=("full" "light" "server")
CONTAINER_ERR=""
for type in "${TYPES[@]}"; do
for variant in "${VARIANTS[@]}"; do
tag="${type}${variant}-${NIGHTLY_TAG}"
STATUS="$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${GHCR_TOKEN}" \
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" \
"https://ghcr.io/v2/${CONTAINER_REPO}/manifests/${tag}")"
if [[ "${STATUS}" == "200" ]]; then
echo " ${tag} - OK"
else
echo " ${tag} - MISSING"
CONTAINER_ERR+=" ${tag}"
fi
done
done
if [[ -n "${CONTAINER_ERR}" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo "Warning: missing container images for ${NIGHTLY_TAG}:${CONTAINER_ERR} (dry run, continuing)."
CHECKS_PASSED=false
else
echo "Error: missing container images for ${NIGHTLY_TAG}:${CONTAINER_ERR}"
echo "The Docker workflow must complete successfully before making a release."
exit 1
fi
else
echo "All container images found for ${NIGHTLY_TAG} - OK"
fi
fi
echo "Checking API/ABI compatibility..."
set +e
bash "$SCRIPT_DIR/check-release-apiabi.sh"
APIABI_RESULT=$?
set -e
if [[ $APIABI_RESULT -ne 0 ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo "Warning: API/ABI check found backwards-incompatible changes (dry run, continuing)."
CHECKS_PASSED=false
else
echo "Error: API/ABI check found backwards-incompatible changes."
exit 1
fi
else
echo "API/ABI compatibility check passed - OK"
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "checks_passed=${CHECKS_PASSED}" >> "$GITHUB_OUTPUT"
fi
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# Generate the description of a release: the previous release version, the
# change log and the link to the nightly release corresponding to the commit being released.
#
# Usage: make-release-desc.sh <version>
# <version>: current release version (v<maj>.<min>.<pat>, the leading v is optional)
#
# The previous version is the highest plain semver tag (v<maj>.<min>.<pat>)
# strictly below <version>. The change log lists all commits between the
# previous version tag and the release commit, one line per commit.
#
# The release commit is the commit <version> points at when the tag exists,
# HEAD otherwise. The nightly release is the b* tag pointing at that commit
# (release.yml tags the same commit); the link is only generated when that
# tag exists.
#
# Env (when running in GitHub Actions):
# GITHUB_OUTPUT: previous_tag, changelog_title, changelog, nightly and nightly_tag
# are written here
# GITHUB_REPOSITORY: owner/repo, used to build the nightly release URL (skipped when unset)
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $(basename "$0") <version>"
exit 1
fi
VERSION="$1"
# Accept the version with or without the leading v, reject anything else
if [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="v${VERSION}"
elif [[ ! "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: invalid version '${VERSION}' (expected v<maj>.<min>.<pat>)"
exit 1
fi
# Make sure all remote tags are available locally (skipped on local runs without origin)
if ! git fetch --tags origin 2>/dev/null; then
echo "Warning: could not fetch tags from origin (local run?)"
fi
# Canonical https URL of this repository (from the origin remote), used to link the previous release.
# Left empty on local runs without an origin remote.
if ORIGIN_URL="$(git remote get-url origin 2>/dev/null)"; then
REPO_URL="https://$(printf '%s' "${ORIGIN_URL}" \
| sed -E -e 's#^git@([^:]+):#https://\1/#' -e 's#^https?://##' -e 's#\.git$##')"
else
REPO_URL=""
fi
# Release commit: the commit <version> points at when the tag exists, HEAD otherwise.
if ! RELEASE_COMMIT="$(git rev-parse -q --verify "refs/tags/${VERSION}^{commit}" 2>/dev/null)"; then
RELEASE_COMMIT="$(git rev-parse HEAD)"
fi
echo "Release commit: $(git rev-parse --short "${RELEASE_COMMIT}")"
PREV="$( { git tag --list; echo "${VERSION}"; } \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| awk -v cur="${VERSION}" '$0 == cur { exit } { prev = $0 } END { print prev }')"
if [[ -n "${PREV}" ]]; then
CHANGELOG="$(git log --oneline "${PREV}..${RELEASE_COMMIT}")"
if [[ -n "${REPO_URL}" ]]; then
CHANGELOG_TITLE="Changelog since [${PREV}](${REPO_URL}/releases/tag/${PREV})"
else
CHANGELOG_TITLE="Changelog since ${PREV}"
fi
else
CHANGELOG="(no previous release tag found)"
CHANGELOG_TITLE="Changelog"
fi
# Nightly release: the b* tag pointing at the release commit (|| true: no match is not an error)
NIGHTLY_TAG="$(git tag --points-at "${RELEASE_COMMIT}" | grep -E '(^|-)b[0-9]+(-[0-9a-f]{7})?$' | head -n 1 || true)"
NIGHTLY=""
if [[ -n "${NIGHTLY_TAG}" ]]; then
if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then
NIGHTLY_URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${NIGHTLY_TAG}"
NIGHTLY="**Nightly build:** [${NIGHTLY_TAG}](${NIGHTLY_URL})"
echo "Nightly release: ${NIGHTLY_URL}"
fi
else
echo "No nightly release found for commit $(git rev-parse --short "${RELEASE_COMMIT}")"
fi
echo "Previous version: ${PREV:-none}"
echo "${CHANGELOG}"
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
{
echo "previous_tag=${PREV}"
echo "changelog_title=${CHANGELOG_TITLE}"
echo "nightly=${NIGHTLY}"
echo "nightly_tag=${NIGHTLY_TAG}"
echo "changelog<<CHANGELOG_EOF"
echo "${CHANGELOG}"
echo "CHANGELOG_EOF"
} >> "${GITHUB_OUTPUT}"
fi
@@ -0,0 +1,55 @@
Take a look at the changelog between the current version and the previous version - use the `./scripts/make-release-desc.sh [current-version]` to obtain it.
Write a summary of the change log in a few sections:
```
## Overview
[a single paragraph overview of all changes]
### Highlights (if applicable)
[go through all Pull Requests and write short summaries of those that have the "highlight" label]
### API changes (if applicable)
[summarize any API changes to `/include/*`, `/tools/mtmd/mtmd.h` and `/tools/server`]
### New models (if applicable)
[summarize new models added to the `src/models/` directory]
### Core changes (if applicable)
[summarize the changes to `/src/*`
### Multi-modality changes (if applicable)
[summarize the changes to `/tools/mtmd/`]
### Server changes (if applicable)
[summarize the changes to `/tools/server/`]
### UI changes (if applicable)
[summarize the changes to `/tools/ui/`]
### ggml changes (if applicable)
[if the version has been updated, link to the respective `ggml` releases on Github, f.ex `https://github.com/ggml-org/ggml/releases/tag/v0.22.0`. for each version bump, lookup the release description and copy the summary here]
```
Guidelines:
- All bullet point in the summary should be concise and rarely exceed a single line of 120 characters (excluding PR links)
- Do not wrap or break lines within a bullet point or paragraph - each bullet point must fit on a single line (PR links allowed to exceed the line limit)
- Avoid repeating `ggml`-specific changes - these should already be covered by the `ggml` release links
- Provide PR link for each bullet point where possible
- Don't add bullet point to state that there are no API changes in some module
- Group similar/related items into a single bullet with multiple trailing PR links, one bullet per topic
- A full list of commits will be appended independently at end of the summary, so it is not a problem to skip some less important changes
- Skip minor-impact notes (e.g. "fix compile warnings", "refactored code", ...)
Output just the summary in a markdown block, without any extra text. Save it to a local text file called `release-notes-vX.Y.Z.txt`.
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# initialize a new worktree from a PR number:
#
# - creates a new remote using the fork's clone URL
# - creates a local branch tracking the remote branch
# - creates a new worktree in a parent folder, suffixed with "-pr-$PR"
#
# sample usage:
# ./scripts/pr2wt.sh 12345
# ./scripts/pr2wt.sh 12345 opencode
# ./scripts/pr2wt.sh 12345 "cmake -B build && cmake --build build"
# ./scripts/pr2wt.sh 12345 "bash -l"
function usage() {
echo "usage: $0 <pr_number> [cmd]"
exit 1
}
# check we are in the right directory
if [[ ! -f "scripts/pr2wt.sh" ]]; then
echo "error: this script must be run from the root of the repository"
exit 1
fi
if [[ $# -lt 1 || $# -gt 2 ]]; then
usage
fi
PR=$1
[[ "$PR" =~ ^[0-9]+$ ]] || { echo "error: PR number must be numeric"; exit 1; }
url_origin=$(git config --get remote.upstream.url 2>/dev/null) || \
url_origin=$(git config --get remote.origin.url) || {
echo "error: no remote named 'upstream' or 'origin' in this repository"
exit 1
}
# Extract org/repo from either https or ssh format.
if [[ $url_origin =~ ^git@ ]]; then
org_repo=$(echo $url_origin | cut -d: -f2)
else
org_repo=$(echo $url_origin | cut -d/ -f4-)
fi
org_repo=${org_repo%.git}
echo "org/repo: $org_repo"
meta=$(curl -sSLf -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$org_repo/pulls/$PR")
if [[ $url_origin =~ ^git@ ]]; then
url_remote=$(echo "$meta" | jq -r '.head.repo.ssh_url')
else
url_remote=$(echo "$meta" | jq -r '.head.repo.clone_url')
fi
head_ref=$(echo "$meta" | jq -r '.head.ref')
echo "url: $url_remote"
echo "head_ref: $head_ref"
url_remote_cur=$(git config --get "remote.pr/$PR.url" 2>/dev/null || true)
if [[ "$url_remote_cur" != "$url_remote" ]]; then
git remote rm pr/$PR 2> /dev/null
git remote add pr/$PR "$url_remote"
fi
git fetch "pr/$PR" "$head_ref"
dir=$(basename $(pwd))
git branch -D pr/$PR 2> /dev/null
git worktree add -b pr/$PR ../$dir-pr-$PR pr/$PR/$head_ref 2> /dev/null
wt_path=$(cd ../$dir-pr-$PR && pwd)
echo "git worktree created in $wt_path"
cd $wt_path
git branch --set-upstream-to=pr/$PR/$head_ref
git pull --ff-only || {
echo "error: failed to pull pr/$PR"
exit 1
}
if [[ $# -eq 2 ]]; then
echo "executing: $2"
eval "$2"
fi
+204
View File
@@ -0,0 +1,204 @@
#!/bin/bash
#
# Release preparation script for llama.cpp.
#
# Bumps the version in CMakeLists.txt on a release candidate branch.
# The branch should then be pushed and a PR created, reviewed, and
# merged. After the PR is merged and the build-cpu workflow has
# completed successfully, the release is finalized by the make-release
# workflow (.github/workflows/make-release.yml), which creates the tag.
#
# Usage:
# ./scripts/release.sh [major|minor|patch] [--dry-run]
#
# Example:
# $ ./scripts/release.sh minor
#
# The script:
# 1. Creates a release candidate branch (llama-rc-v<major>.<minor>.<patch>)
# 2. Bumps the version in CMakeLists.txt
# 3. Commits the version bump
#
set -e
if [ ! -f "CMakeLists.txt" ] || [ ! -d "scripts" ]; then
echo "Error: Must be run from llama.cpp root directory"
exit 1
fi
# Parse command line arguments
VERSION_TYPE=""
DRY_RUN=false
for arg in "$@"; do
case $arg in
--dry-run)
DRY_RUN=true
;;
major|minor|patch)
VERSION_TYPE="$arg"
;;
*)
echo "Error: Unknown argument '$arg'"
echo "Usage: $0 [major|minor|patch] [--dry-run]"
exit 1
;;
esac
done
# Default to patch if no version type specified
VERSION_TYPE="${VERSION_TYPE:-patch}"
# Common validation functions
check_git_status() {
# Check for uncommitted changes (skip in dry-run)
if [ "$DRY_RUN" = false ] && ! git diff-index --quiet HEAD --; then
echo "Error: You have uncommitted changes. Please commit or stash them first."
exit 1
fi
}
check_master_branch() {
# Ensure we're on master branch
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" != "master" ]; then
if [ "$DRY_RUN" = true ]; then
echo "[dry run] Warning: Not on master branch (currently on: $CURRENT_BRANCH). Continuing with dry-run..."
echo ""
else
echo "Error: Must be on master branch. Currently on: $CURRENT_BRANCH"
exit 1
fi
fi
}
check_master_up_to_date() {
# Check if we have the latest from master (skip in dry-run)
if [ "$DRY_RUN" = false ]; then
echo "Checking if local master is up-to-date with remote..."
git fetch origin master
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/master)
if [ "$LOCAL" != "$REMOTE" ]; then
echo "Error: Your local master branch is not up-to-date with origin/master."
echo "Please run 'git pull origin master' first."
exit 1
fi
echo "✓ Local master is up-to-date with remote"
echo ""
elif [ "$(git branch --show-current)" = "master" ]; then
echo "[dry run] Warning: Dry-run mode - not checking if master is up-to-date with remote"
echo ""
fi
}
# In-place sed that works on both GNU (Linux) and BSD (macOS) sed
sed_inplace() {
if sed --version >/dev/null 2>&1; then
sed -i "$@"
else
sed -i '' "$@"
fi
}
prepare_release() {
if [ "$DRY_RUN" = true ]; then
echo "[dry-run] Preparing release (no changes will be made)"
else
echo "Starting release preparation..."
fi
echo ""
check_git_status
check_master_branch
check_master_up_to_date
# Extract current version from CMakeLists.txt
echo "Step 1: Reading current version..."
MAJOR=$(grep "set(LLAMA_VERSION_MAJOR" CMakeLists.txt | sed 's/.*MAJOR \([0-9]*\).*/\1/')
MINOR=$(grep "set(LLAMA_VERSION_MINOR" CMakeLists.txt | sed 's/.*MINOR \([0-9]*\).*/\1/')
PATCH=$(grep "set(LLAMA_VERSION_PATCH" CMakeLists.txt | sed 's/.*PATCH \([0-9]*\).*/\1/')
echo "Current version: $MAJOR.$MINOR.$PATCH"
# Calculate new version
case $VERSION_TYPE in
major)
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
;;
minor)
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
;;
patch)
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH"
RC_BRANCH="llama-rc-v$NEW_VERSION"
echo "New release version: $NEW_VERSION"
echo "Release candidate branch: $RC_BRANCH"
echo ""
# Create release candidate branch
echo "Step 2: Creating release candidate branch..."
if [ "$DRY_RUN" = true ]; then
echo " [dry-run] Would create branch: $RC_BRANCH"
else
git checkout -b "$RC_BRANCH"
echo "✓ Created and switched to branch: $RC_BRANCH"
fi
echo ""
# Update CMakeLists.txt for release
echo "Step 3: Updating version in CMakeLists.txt..."
if [ "$DRY_RUN" = true ]; then
echo " [dry-run] Would update LLAMA_VERSION_MAJOR to $NEW_MAJOR"
echo " [dry-run] Would update LLAMA_VERSION_MINOR to $NEW_MINOR"
echo " [dry-run] Would update LLAMA_VERSION_PATCH to $NEW_PATCH"
else
sed_inplace -e "s/set(LLAMA_VERSION_MAJOR [0-9]*)/set(LLAMA_VERSION_MAJOR $NEW_MAJOR)/" CMakeLists.txt
sed_inplace -e "s/set(LLAMA_VERSION_MINOR [0-9]*)/set(LLAMA_VERSION_MINOR $NEW_MINOR)/" CMakeLists.txt
sed_inplace -e "s/set(LLAMA_VERSION_PATCH [0-9]*)/set(LLAMA_VERSION_PATCH $NEW_PATCH)/" CMakeLists.txt
fi
echo ""
# Commit version bump
echo "Step 4: Committing version bump..."
if [ "$DRY_RUN" = true ]; then
echo " [dry-run] Would commit: 'llama.cpp : bump version to $NEW_VERSION'"
else
git add CMakeLists.txt
git commit -m "llama.cpp : bump version to $NEW_VERSION"
fi
echo ""
echo ""
if [ "$DRY_RUN" = true ]; then
echo "[dry-run] Summary (no changes were made):"
echo " • Would have created branch: $RC_BRANCH"
echo " • Would have updated version to: $NEW_VERSION"
else
echo "Release preparation completed!"
echo "Summary:"
echo " • Created branch: $RC_BRANCH"
echo " • Updated version to: $NEW_VERSION"
echo ""
echo "Next steps:"
echo " • Push branch to remote: git push origin $RC_BRANCH"
echo " • Create a Pull Request from $RC_BRANCH to master"
echo " • After the PR is merged and the build-cpu workflow has passed,"
echo " create the release with the make-release workflow"
echo " (.github/workflows/make-release.yml)"
fi
}
prepare_release
+110
View File
@@ -0,0 +1,110 @@
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
// This file is used for testing wasm build from emscripten
// Example build command:
// emcmake cmake -B build-wasm -DGGML_WEBGPU=ON -DLLAMA_OPENSSL=OFF
// cmake --build build-wasm --target test-backend-ops -j
const PORT = 8080;
const STATIC_DIR = path.join(__dirname, '../build-wasm/bin');
console.log(`Serving static files from: ${STATIC_DIR}`);
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.json': 'application/json',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
};
async function generateDirListing(dirPath, reqUrl) {
const files = await fs.readdir(dirPath);
let html = `
<!DOCTYPE html>
<html>
<head>
<title>Directory Listing</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
ul { list-style: none; padding: 0; }
li { margin: 5px 0; }
a { text-decoration: none; color: #0066cc; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Directory: ${reqUrl}</h1>
<ul>
`;
if (reqUrl !== '/') {
html += `<li><a href="../">../ (Parent Directory)</a></li>`;
}
for (const file of files) {
const filePath = path.join(dirPath, file);
const stats = await fs.stat(filePath);
const link = encodeURIComponent(file) + (stats.isDirectory() ? '/' : '');
html += `<li><a href="${link}">${file}${stats.isDirectory() ? '/' : ''}</a></li>`;
}
html += `
</ul>
</body>
</html>
`;
return html;
}
const server = http.createServer(async (req, res) => {
try {
// Set COOP and COEP headers
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
const filePath = path.join(STATIC_DIR, decodeURIComponent(req.url));
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
const indexPath = path.join(filePath, 'index.html');
try {
const indexData = await fs.readFile(indexPath);
res.writeHeader(200, { 'Content-Type': 'text/html' });
res.end(indexData);
} catch {
// No index.html, generate directory listing
const dirListing = await generateDirListing(filePath, req.url);
res.writeHeader(200, { 'Content-Type': 'text/html' });
res.end(dirListing);
}
} else {
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
const data = await fs.readFile(filePath);
res.writeHeader(200, { 'Content-Type': contentType });
res.end(data);
}
} catch (err) {
if (err.code === 'ENOENT') {
res.writeHeader(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
} else {
res.writeHeader(500, { 'Content-Type': 'text/plain' });
res.end('500 Internal Server Error');
}
}
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env python3
import argparse
import json
import os
import random
import sqlite3
import subprocess
from time import sleep, time
from typing import Optional, Union
import datasets
import logging
import matplotlib.pyplot as plt
import numpy as np
import requests
from tqdm.contrib.concurrent import thread_map
logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger("server-bench")
def get_prompts_text(dataset_name: str, n_prompts: int) -> Optional[list[str]]:
ret = []
if dataset_name.lower() == "mmlu":
logger.info("Loading MMLU dataset...")
ret = datasets.load_dataset("cais/mmlu", "all")["test"]["question"]
else:
return None
if n_prompts >= 0:
ret = ret[:n_prompts]
return ret
def get_prompt_lengths_rng(n_prompts: int, prompt_length_min: int, prompt_length_max: int, seed_offset: int) -> list[int]:
assert n_prompts >= 0
ret: list[int] = []
for i in range(n_prompts):
if seed_offset >= 0:
random.seed(3 * (seed_offset + 1000 * i) + 0)
ret.append(random.randint(prompt_length_min, prompt_length_max))
return ret
def get_prompts_rng(prompt_lengths: list[int]) -> list[list[int]]:
return [[random.randint(100, 10000) for _ in range(pl)] for pl in prompt_lengths]
def get_server(path_server: str, path_log: Optional[str]) -> dict:
if path_server.startswith("http://") or path_server.startswith("https://"):
return {"process": None, "address": path_server, "fout": None}
if os.environ.get("LLAMA_ARG_HOST") is None:
logger.info("LLAMA_ARG_HOST not explicitly set, using 127.0.0.1")
os.environ["LLAMA_ARG_HOST"] = "127.0.0.1"
if os.environ.get("LLAMA_ARG_PORT") is None:
logger.info("LLAMA_ARG_PORT not explicitly set, using 9931")
os.environ["LLAMA_ARG_PORT"] = "9931"
hostname: Optional[str] = os.environ.get("LLAMA_ARG_HOST")
port: Optional[str] = os.environ.get("LLAMA_ARG_PORT")
assert hostname is not None
assert port is not None
address: str = f"http://{hostname}:{port}"
logger.info(f"Starting the llama.cpp server under {address}...")
fout = open(path_log.format(port=port), "w") if path_log is not None else subprocess.DEVNULL
process = subprocess.Popen([path_server], stdout=fout, stderr=subprocess.STDOUT)
n_failures: int = 0
while True:
try:
sleep(1.0)
exit_code = process.poll()
if exit_code is not None:
raise RuntimeError(f"llama.cpp server exited unexpectedly with exit code {exit_code}{path_log and f', see {path_log.format(port=port)}' or ''}")
response = requests.get(f"{address}/health")
if response.status_code == 200:
break
except requests.ConnectionError:
n_failures += 1
if n_failures >= 10:
raise RuntimeError("llama.cpp server is not healthy after 10 seconds")
return {"process": process, "address": address, "fout": fout}
def get_prompt_length(data: dict) -> int:
session = data["session"]
server_address: str = data["server_address"]
response = session.post(
f"{server_address}/apply-template",
json={"messages": [{"role": "user", "content": data["prompt"], "stream": True}]}
)
response.raise_for_status()
prompt: str = json.loads(response.text)["prompt"]
response = session.post(
f"{server_address}/tokenize",
json={"content": prompt, "add_special": True}
)
response.raise_for_status()
tokens: list[str] = json.loads(response.text)["tokens"]
return len(tokens)
def send_prompt(data: dict) -> tuple[float, list[float]]:
session = data["session"]
server_address: str = data["server_address"]
t_submit = time()
if data["external_server"]:
json_data: dict = {
"prompt": data["prompt"], "ignore_eos": True,
"seed": data["seed"], "max_tokens": data["n_predict"], "stream": True}
response = session.post(f"{server_address}/v1/completions", json=json_data, stream=True)
elif data["synthetic_prompt"]:
json_data: dict = {
"prompt": data["prompt"], "ignore_eos": True, "cache_prompt": False,
"seed": data["seed"], "n_predict": data["n_predict"], "stream": True}
response = session.post(f"{server_address}/completion", json=json_data, stream=True)
else:
response = session.post(
f"{server_address}/apply-template",
json={"messages": [{"role": "user", "content": data["prompt"], "stream": True}]}
)
response.raise_for_status()
prompt: str = json.loads(response.text)["prompt"]
json_data: dict = {"prompt": prompt, "seed": data["seed"], "n_predict": data["n_predict"], "stream": True}
response = session.post(f"{server_address}/completion", json=json_data, stream=True)
response.raise_for_status()
lines = []
token_arrival_times: list[float] = []
for line in response.iter_lines(decode_unicode=False):
if not line.startswith(b"data: "):
continue
lines.append(line)
token_arrival_times.append(time())
token_arrival_times = token_arrival_times[:-1]
if len(lines) > 1 and "timings" in json.loads(lines[-2][6:]):
token_arrival_times = token_arrival_times[:-1]
return (t_submit, token_arrival_times)
def benchmark(
path_server: str, path_log: Optional[str], path_db: Optional[str], name: Optional[str], prompt_source: str, n_prompts: int,
n_predict: int, n_predict_min: int, seed_offset: int):
external_server: bool = path_server.startswith("http://") or path_server.startswith("https://")
if os.environ.get("LLAMA_ARG_N_PARALLEL") is None:
logger.info("LLAMA_ARG_N_PARALLEL not explicitly set, using 32")
os.environ["LLAMA_ARG_N_PARALLEL"] = "32"
parallel: int = int(os.environ.get("LLAMA_ARG_N_PARALLEL")) # type: ignore
prompts: Union[None, list[str], list[list[int]]] = get_prompts_text(prompt_source, n_prompts)
synthetic_prompts: bool = prompts is None
prompt_n = []
if synthetic_prompts:
prompt_source_split: list[str] = prompt_source.split("-")
assert len(prompt_source_split) == 3
assert prompt_source_split[0].lower() == "rng"
prompt_length_min: int = int(prompt_source_split[1])
prompt_length_max: int = int(prompt_source_split[2])
logger.info("Generating random prompts...")
prompt_n = get_prompt_lengths_rng(n_prompts, prompt_length_min, prompt_length_max, seed_offset)
prompts = get_prompts_rng(prompt_n)
else:
n_predict_min = n_predict
if not external_server and os.environ.get("LLAMA_ARG_CTX_SIZE") is None:
context_per_slot: int = int(1.05 * (n_predict + (np.max(prompt_n) if synthetic_prompts else 2048)))
context_total: int = context_per_slot * parallel
os.environ["LLAMA_ARG_CTX_SIZE"] = str(context_total)
logger.info(f"LLAMA_ARG_CTX_SIZE not explicitly set, using {context_total} ({context_per_slot} per slot).")
server: Optional[dict] = None
session = None
try:
server = get_server(path_server, path_log)
server_address: str = server["address"]
assert external_server == (server["process"] is None)
adapter = requests.adapters.HTTPAdapter(pool_connections=parallel, pool_maxsize=parallel) # type: ignore
session = requests.Session()
session.mount("http://", adapter)
session.mount("https://", adapter)
data: list[dict] = []
assert isinstance(prompts, list)
for i, p in enumerate(prompts):
if seed_offset >= 0:
random.seed(3 * (seed_offset + 1000 * i) + 1)
data.append({
"session": session, "server_address": server_address, "external_server": external_server, "prompt": p,
"synthetic_prompt": synthetic_prompts, "n_predict": random.randint(n_predict_min, n_predict),
"seed": (3 * (seed_offset + 1000 * i) + 2) if seed_offset >= 0 else -1})
if not synthetic_prompts:
logger.info("Getting the prompt lengths...")
prompt_n = [get_prompt_length(d) for d in data]
logger.info("Starting the benchmark...\n")
t0 = time()
results: list[tuple[float, list[float]]] = thread_map(send_prompt, data, max_workers=parallel, chunksize=1)
finally:
if server is not None and server["process"] is not None:
server["process"].terminate()
server["process"].wait()
if session is not None:
session.close()
prompt_t = []
token_t = []
depth_sum: int = 0
for pn, (t_submit, tat) in zip(prompt_n, results):
prompt_t.append(tat[0] - t_submit)
token_t += tat
n_tokens: int = len(tat)
depth_sum += n_tokens * pn
depth_sum += n_tokens * (n_tokens + 1) // 2
assert len(token_t) > 0
prompt_n = np.array(prompt_n, dtype=np.int64)
prompt_t = np.array(prompt_t, dtype=np.float64)
token_t = np.array(token_t, dtype=np.float64)
token_t -= t0
token_t_last = np.max(token_t)
logger.info("")
logger.info(f"Benchmark duration: {token_t_last:.2f} s")
logger.info(f"Request throughput: {n_prompts / token_t_last:.2f} requests/s = {n_prompts / (token_t_last/60):.2f} requests/min")
logger.info(f"Total prompt length: {np.sum(prompt_n)} tokens")
logger.info(f"Average prompt length: {np.mean(prompt_n):.2f} tokens")
logger.info(f"Average prompt latency: {1e3 * np.mean(prompt_t):.2f} ms")
logger.info(f"Average prompt speed: {np.sum(prompt_n) / np.sum(prompt_t):.2f} tokens/s")
logger.info(f"Total generated tokens: {token_t.shape[0]}")
logger.info(f"Average generation depth: {depth_sum / token_t.shape[0]:.2f} tokens")
logger.info(f"Average total generation speed: {token_t.shape[0] / token_t_last:.2f} tokens/s")
logger.info(f"Average generation speed per slot: {token_t.shape[0] / (parallel * token_t_last):.2f} tokens/s / slot")
if path_db is not None:
con = sqlite3.connect(path_db)
cursor = con.cursor()
cursor.execute(
"CREATE TABLE IF NOT EXISTS server_bench"
"(name TEXT, n_parallel INTEGER, prompt_source TEXT, n_prompts INTEGER, "
"n_predict INTEGER, n_predict_min INTEGER, seed_offset INTEGER, runtime REAL);")
cursor.execute(
"INSERT INTO server_bench VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
[name, parallel, prompt_source, n_prompts, n_predict, n_predict_min, seed_offset, token_t_last])
con.commit()
plt.figure()
plt.scatter(prompt_n, 1e3 * prompt_t, s=10.0, marker=".", alpha=0.25)
plt.xlim(0, 1.05e0 * np.max(prompt_n))
plt.ylim(0, 1.05e3 * np.max(prompt_t))
plt.title(name or "")
plt.xlabel("Prompt length [tokens]")
plt.ylabel("Time to first token [ms]")
plt.savefig("prompt_time.png", dpi=240)
bin_max = np.ceil(token_t_last) + 1
plt.figure()
plt.hist(token_t, np.linspace(0, bin_max, bin_max, endpoint=False).tolist())
plt.xlim(0, bin_max + 1)
plt.title(name or "")
plt.xlabel("Time [s]")
plt.ylabel("Num. tokens generated per second")
plt.savefig("gen_rate.png", dpi=240)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Tool for benchmarking the throughput of the llama.cpp HTTP server. "
"Results are printed to console and visualized as plots (saved to current working directory). "
"To pass arguments such as the model path to the server, set the corresponding environment variables (see llama-server --help). "
"The reported numbers are the speeds as observed by the Python script and may differ from the performance reported by the server, "
"particularly when the server is fast vs. the network or Python script (e.g. when serving a very small model).")
parser.add_argument("--path_server", type=str, default="llama-server", help="Path to the llama.cpp server binary")
parser.add_argument("--path_log", type=str, default="server-bench-{port}.log", help="Path to the model to use for the benchmark")
parser.add_argument("--path_db", type=str, default=None, help="Path to an sqlite database to store the benchmark results in")
parser.add_argument("--name", type=str, default=None, help="Name to label plots and database entries with")
parser.add_argument(
"--prompt_source", type=str, default="rng-1024-2048",
help="How to get the prompts for the benchmark, either 'mmlu' for MMLU questions or "
"rng-MIN-MAX for synthetic prompts with random lengths in the interval [MIN, MAX]")
parser.add_argument("--n_prompts", type=int, default=100, help="Number of prompts to evaluate")
parser.add_argument("--n_predict", type=int, default=2048, help="Max. number of tokens to predict per prompt")
parser.add_argument(
"--n_predict_min", type=int, default=1024,
help="Min. number of tokens to predict per prompt (supported for synthetic prompts only)")
parser.add_argument("--seed_offset", type=int, default=0, help="Offset for determining the seeds for pseudorandom prompt/generation lengths. "
"Correlations between seeds can occur when set >= 1000. Negative values mean no seed.")
args = parser.parse_args()
benchmark(**vars(args))
File diff suppressed because it is too large. Load diff
+202
View File
@@ -0,0 +1,202 @@
import argparse
import json
import requests
import logging
import sys
handler = logging.StreamHandler(sys.stdout)
handler.terminator = "" # ← no newline
logging.basicConfig(level=logging.INFO, format='%(message)s', handlers=[handler])
logger = logging.getLogger("server-test-model")
def run_query(url, messages, tools=None, stream=False, tool_choice=None):
payload = {
"messages": messages,
"stream": stream,
"max_tokens": 5000,
}
if tools:
payload["tools"] = tools
if tool_choice:
payload["tool_choice"] = tool_choice
try:
response = requests.post(url, json=payload, stream=stream)
response.raise_for_status()
except requests.exceptions.RequestException as e:
if e.response is not None:
logger.info(f"Response error: {e} for {e.response.content}\n")
else:
logger.info(f"Error connecting to server: {e}\n")
return None
full_content = ""
reasoning_content = ""
tool_calls = []
if stream:
logger.info(f"--- Streaming response (Tools: {bool(tools)}) ---\n")
for line in response.iter_lines():
if line:
decoded_line = line.decode("utf-8")
if decoded_line.startswith("data: "):
data_str = decoded_line[6:]
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
if "choices" in data and len(data["choices"]) > 0:
delta = data["choices"][0].get("delta", {})
# Content
content_chunk = delta.get("content", "")
if content_chunk:
full_content += content_chunk
logger.info(content_chunk)
# Reasoning
reasoning_chunk = delta.get("reasoning_content", "")
if reasoning_chunk:
reasoning_content += reasoning_chunk
logger.info(f"\x1B[3m{reasoning_chunk}\x1B[0m")
# Tool calls
if "tool_calls" in delta:
for tc in delta["tool_calls"]:
index = tc.get("index")
if index is not None:
while len(tool_calls) <= index:
# Using "function" as type default but could be flexible
tool_calls.append(
{
"id": "",
"type": "function",
"function": {
"name": "",
"arguments": "",
},
}
)
if "id" in tc:
tool_calls[index]["id"] += tc["id"]
if "function" in tc:
if "name" in tc["function"]:
tool_calls[index]["function"][
"name"
] += tc["function"]["name"]
if "arguments" in tc["function"]:
tool_calls[index]["function"][
"arguments"
] += tc["function"]["arguments"]
except json.JSONDecodeError:
logger.info(f"Failed to decode JSON: {data_str}\n")
logger.info("\n--- End of Stream ---\n")
else:
logger.info(f"--- Non-streaming response (Tools: {bool(tools)}) ---\n")
data = response.json()
if "choices" in data and len(data["choices"]) > 0:
message = data["choices"][0].get("message", {})
full_content = message.get("content", "")
reasoning_content = message.get("reasoning_content", "")
tool_calls = message.get("tool_calls", [])
logger.info(full_content)
logger.info("--- End of Response ---\n")
return {
"content": full_content,
"reasoning_content": reasoning_content,
"tool_calls": tool_calls,
}
def test_chat(url, stream):
logger.info(f"\n=== Testing Chat (Stream={stream}) ===\n")
messages = [{"role": "user", "content": "What is the capital of France?"}]
result = run_query(url, messages, stream=stream)
if result:
if result["content"]:
logger.info("PASS: Output received.\n")
else:
logger.info("WARN: No content received (valid if strict tool call, but unexpected here).\n")
if result.get("reasoning_content"):
logger.info(f"INFO: Reasoning content detected ({len(result['reasoning_content'])} chars).\n")
else:
logger.info("INFO: No reasoning content detected (Standard model behavior).\n")
else:
logger.info("FAIL: No result.\n")
def test_tool_call(url, stream):
logger.info(f"\n=== Testing Tool Call (Stream={stream}) ===\n")
messages = [
{
"role": "user",
"content": "What is the weather in London? Please use the get_weather tool.",
}
]
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
},
}
]
result = run_query(url, messages, tools=tools, tool_choice="auto", stream=stream)
if result:
tcs = result.get("tool_calls")
if tcs and len(tcs) > 0:
logger.info("PASS: Tool calls detected.")
for tc in tcs:
func = tc.get("function", {})
logger.info(f" Tool: {func.get('name')}, Args: {func.get('arguments')}\n")
else:
logger.info(f"FAIL: No tool calls. Content: {result['content']}\n")
if result.get("reasoning_content"):
logger.info(
f"INFO: Reasoning content detected during tool call ({len(result['reasoning_content'])} chars).\n"
)
else:
logger.info("FAIL: Query failed.\n")
def main():
parser = argparse.ArgumentParser(description="Test llama-server functionality.")
parser.add_argument("--host", default="localhost", help="Server host")
parser.add_argument("--port", default=9931, type=int, help="Server port")
args = parser.parse_args()
base_url = f"http://{args.host}:{args.port}/v1/chat/completions"
logger.info(f"Testing server at {base_url}\n")
# Non-streaming tests
test_chat(base_url, stream=False)
test_tool_call(base_url, stream=False)
# Streaming tests
test_chat(base_url, stream=True)
test_tool_call(base_url, stream=True)
if __name__ == "__main__":
main()
+991
View File
@@ -0,0 +1,991 @@
#!/usr/bin/env python3
"""
Test parallel tool-calling capability via chat completions endpoint.
Only run this against models that actually support parallel tool calls — this
script does not attempt to toggle that setting on the server. Each scenario is
explicitly worded so that a capable model SHOULD emit multiple tool calls in a
single assistant turn (either the same tool N times, or several different
tools at once).
Each test case contains:
- tools: list of tool definitions (OpenAI-compatible)
- messages: initial conversation messages
- mock_tool_responses: dict mapping tool_name -> callable(arguments) -> str (JSON)
- expected_parallel: dict describing what constitutes a successful parallel turn
{"min_parallel": int, # minimum tool_calls in one turn
"require_same_tool": Optional[str], # all parallel calls must be this tool
"require_distinct_tools": Optional[int], # >= N distinct tool names in one turn
"min_distinct_args_key": Optional[str]} # parallel calls must span this
# many distinct values of this arg key
- validate: callable(turns, all_tool_calls, final_content) -> (passed, reason)
"""
import argparse
import json
import requests
import sys
# ---------------------------------------------------------------------------
# Color / formatting helpers
# ---------------------------------------------------------------------------
RESET = "\x1b[0m"
BOLD = "\x1b[1m"
DIM = "\x1b[2m"
CYAN = "\x1b[36m"
YELLOW = "\x1b[33m"
GREEN = "\x1b[32m"
RED = "\x1b[31m"
BLUE = "\x1b[34m"
WHITE = "\x1b[97m"
MAGENTA = "\x1b[35m"
def _print(text="", end="\n"):
sys.stdout.write(text + end)
sys.stdout.flush()
def print_header(title):
bar = "─" * 60
_print(f"\n{BOLD}{CYAN}┌{bar}┐{RESET}")
_print(
f"{BOLD}{CYAN}│ {WHITE}{title}{CYAN}{' ' * max(0, 58 - len(title))}│{RESET}"
)
_print(f"{BOLD}{CYAN}└{bar}┘{RESET}")
def print_turn_banner(turn_idx, n_calls):
color = MAGENTA if n_calls >= 2 else DIM
_print(f"\n {BOLD}{color}▶ turn {turn_idx} — {n_calls} tool call(s){RESET}")
def print_tool_call(name, args):
args_str = json.dumps(args)
_print(
f" {BOLD}{YELLOW}⚙ {name}{RESET}{DIM}({args_str}){RESET}"
)
def print_tool_result(result):
preview = result[:140] + ("…" if len(result) > 140 else "")
_print(f" {DIM}{BLUE}↳ {preview}{RESET}")
def print_model_output(text):
sys.stdout.write(text)
sys.stdout.flush()
def print_pass(reason):
_print(f"\n{BOLD}{GREEN}✔ PASS{RESET} {reason}")
def print_fail(reason):
_print(f"\n{BOLD}{RED}✘ FAIL{RESET} {reason}")
def print_info(msg):
_print(f"{DIM}{msg}{RESET}")
def print_warn(msg):
_print(f"{BOLD}{YELLOW}⚠ {msg}{RESET}")
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
def chat_completion(url, messages, tools=None, stream=False):
payload = {
"messages": messages,
"stream": stream,
"max_tokens": 4096,
}
if tools:
payload["tools"] = tools
payload["tool_choice"] = "auto"
try:
response = requests.post(url, json=payload, stream=stream)
response.raise_for_status()
except requests.exceptions.RequestException as e:
body = e.response.content if (e.response is not None) else b""
print_fail(f"Request error: {e} | body: {body}")
return None
full_content = ""
reasoning_content = ""
tool_calls: list[dict] = []
if stream:
for line in response.iter_lines():
if not line:
continue
decoded = line.decode("utf-8")
if not decoded.startswith("data: "):
continue
data_str = decoded[6:]
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
except json.JSONDecodeError:
continue
choices = data.get("choices", [])
if not choices:
continue
delta = choices[0].get("delta", {})
if delta.get("reasoning_content"):
reasoning_content += delta["reasoning_content"]
if delta.get("content"):
full_content += delta["content"]
print_model_output(delta["content"])
for tc in delta.get("tool_calls", []):
idx = tc.get("index", 0)
while len(tool_calls) <= idx:
tool_calls.append(
{
"id": "",
"type": "function",
"function": {"name": "", "arguments": ""},
}
)
if "id" in tc:
tool_calls[idx]["id"] += tc["id"]
if "function" in tc:
if "name" in tc["function"]:
tool_calls[idx]["function"]["name"] += tc["function"]["name"]
if "arguments" in tc["function"]:
tool_calls[idx]["function"]["arguments"] += tc["function"][
"arguments"
]
else:
data = response.json()
choices = data.get("choices", [])
if choices:
msg = choices[0].get("message", {})
full_content = msg.get("content") or ""
reasoning_content = msg.get("reasoning_content") or ""
tool_calls = msg.get("tool_calls") or []
if full_content:
print_model_output(full_content)
result = {"content": full_content, "tool_calls": tool_calls}
if reasoning_content:
result["reasoning_content"] = reasoning_content
return result
def run_agentic_loop(url, messages, tools, mock_tool_responses, stream, max_turns=6):
"""
Drive the multi-turn tool-call loop, but record each turn's tool calls
separately so parallelism can be validated.
Returns (turns, all_tool_calls, final_content) where `turns` is a list
of dicts: {"index": int, "tool_calls": [...], "content": str}.
"""
msgs = list(messages)
turns: list[dict] = []
all_tool_calls: list[dict] = []
for turn_idx in range(max_turns):
result = chat_completion(url, msgs, tools=tools, stream=stream)
if result is None:
return turns, all_tool_calls, None
tcs = result.get("tool_calls") or []
content = result.get("content") or ""
turns.append(
{"index": turn_idx, "tool_calls": list(tcs), "content": content}
)
if not tcs:
if content:
_print(f"\n{DIM}{'·' * 60}{RESET}")
_print(f"{DIM} model response:{RESET}\n")
return turns, all_tool_calls, content
print_turn_banner(turn_idx, len(tcs))
all_tool_calls.extend(tcs)
assistant_msg: dict = {
"role": "assistant",
"content": content,
"tool_calls": tcs,
}
reasoning = result.get("reasoning_content")
if reasoning:
assistant_msg["reasoning_content"] = reasoning
msgs.append(assistant_msg)
for tc in tcs:
tool_name = tc["function"]["name"]
try:
args = json.loads(tc["function"]["arguments"])
except json.JSONDecodeError:
args = {}
print_tool_call(tool_name, args)
mock_fn = mock_tool_responses.get(tool_name)
if mock_fn:
tool_result = mock_fn(args)
else:
tool_result = json.dumps({"error": f"Unknown tool: {tool_name}"})
print_tool_result(tool_result)
msgs.append(
{
"role": "tool",
"tool_call_id": tc.get("id", ""),
"content": tool_result,
}
)
return turns, all_tool_calls, None
# ---------------------------------------------------------------------------
# Parallelism helpers
# ---------------------------------------------------------------------------
def _best_parallel_turn(turns):
"""Return the turn (dict) with the most tool calls, or None if no tools."""
tool_turns = [t for t in turns if t["tool_calls"]]
if not tool_turns:
return None
return max(tool_turns, key=lambda t: len(t["tool_calls"]))
def _distinct_tool_names(turn):
return {tc["function"]["name"] for tc in turn["tool_calls"]}
def _distinct_arg_values(turn, key):
values = set()
for tc in turn["tool_calls"]:
try:
args = json.loads(tc["function"]["arguments"])
except json.JSONDecodeError:
continue
v = args.get(key)
if v is not None:
if isinstance(v, str):
values.add(v.strip().lower())
else:
values.add(v)
return values
def _check_parallel(turns, expected):
"""
Check that at least one turn satisfies the parallel-call expectations.
Returns (ok, reason).
"""
best = _best_parallel_turn(turns)
if best is None:
return False, "No tool calls were made at all"
min_parallel = expected.get("min_parallel", 2)
if len(best["tool_calls"]) < min_parallel:
by_turn = [len(t["tool_calls"]) for t in turns]
return False, (
f"No turn had >= {min_parallel} parallel tool calls "
f"(per-turn counts: {by_turn})"
)
require_same = expected.get("require_same_tool")
if require_same is not None:
names = [tc["function"]["name"] for tc in best["tool_calls"]]
if any(n != require_same for n in names):
return False, (
f"Parallel turn mixed tools; expected all {require_same!r}, got {names}"
)
require_distinct = expected.get("require_distinct_tools")
if require_distinct is not None:
distinct = _distinct_tool_names(best)
if len(distinct) < require_distinct:
return False, (
f"Parallel turn had only {len(distinct)} distinct tool names "
f"({distinct}); need >= {require_distinct}"
)
distinct_key = expected.get("min_distinct_args_key")
distinct_count = expected.get("min_distinct_args_count", min_parallel)
if distinct_key is not None:
values = _distinct_arg_values(best, distinct_key)
if len(values) < distinct_count:
return False, (
f"Parallel turn had only {len(values)} distinct {distinct_key!r} "
f"values ({values}); need >= {distinct_count}"
)
return True, (
f"Parallel turn had {len(best['tool_calls'])} calls across "
f"{len(_distinct_tool_names(best))} distinct tool(s)"
)
# ---------------------------------------------------------------------------
# Test case runner
# ---------------------------------------------------------------------------
def run_test(url, test_case, stream):
name = test_case["name"]
mode = f"{'stream' if stream else 'non-stream'}"
print_header(f"{name} [{mode}]")
turns, all_tool_calls, final_content = run_agentic_loop(
url,
messages=test_case["messages"],
tools=test_case["tools"],
mock_tool_responses=test_case["mock_tool_responses"],
stream=stream,
)
if not turns:
print_fail("No response from server.")
return False
parallel_ok, parallel_reason = _check_parallel(turns, test_case["expected_parallel"])
if not parallel_ok:
print_fail(parallel_reason)
return False
passed, reason = test_case["validate"](turns, all_tool_calls, final_content)
if passed:
print_pass(f"{parallel_reason}; {reason}")
else:
print_fail(reason)
return passed
# ---------------------------------------------------------------------------
# Test case definitions
# ---------------------------------------------------------------------------
# ---- Test 1: Multi-file read (same tool, multiple distinct paths) ----
_FILE_TOOLS = [
{
"type": "function",
"function": {
"name": "read_file",
"description": (
"Read the full contents of a file from the local filesystem. "
"Call this tool in parallel when asked to read several files — "
"each path needs its own call."
),
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or repo-relative path to a file",
},
},
"required": ["path"],
},
},
},
]
_FILE_CONTENTS = {
"config/database.yml": "host: db.internal\nport: 5432\nuser: svc_app\n",
"config/redis.yml": "host: cache.internal\nport: 6379\ndb: 0\n",
"config/queue.yml": "broker: rabbitmq.internal\nport: 5672\nvhost: prod\n",
"config/auth.yml": "provider: oidc\nissuer: https://auth.internal\n",
}
def _read_file_mock(args):
path = args.get("path", "")
norm = path.lstrip("./").lstrip("/")
content = _FILE_CONTENTS.get(norm)
if content is None:
for k, v in _FILE_CONTENTS.items():
if path.endswith(k):
content = v
break
if content is None:
return json.dumps({"path": path, "error": "not found"})
return json.dumps({"path": path, "content": content})
MULTIFILE_READ_TEST = {
"name": "Parallel multi-file read (same tool, 4 distinct paths)",
"tools": _FILE_TOOLS,
"messages": [
{
"role": "user",
"content": (
"Please read all four of these config files so I can review them "
"together: config/database.yml, config/redis.yml, config/queue.yml, "
"and config/auth.yml. Call read_file for every path in parallel in "
"a single batch — do NOT read them one by one sequentially across "
"turns. After you have all four, give me a one-line summary of each."
),
}
],
"mock_tool_responses": {"read_file": _read_file_mock},
"expected_parallel": {
"min_parallel": 4,
"require_same_tool": "read_file",
"min_distinct_args_key": "path",
"min_distinct_args_count": 4,
},
"validate": lambda turns, tcs, content: _validate_multifile(turns, tcs, content),
}
def _validate_multifile(turns, tcs, content):
del turns
if not content:
return False, "No final summary produced"
return True, f"{len(tcs)} total read_file calls; content length={len(content)}"
# ---- Test 2: Batch TODO marking (same tool, N calls in one turn) ----
_TODO_TOOLS = [
{
"type": "function",
"function": {
"name": "mark_todo_complete",
"description": (
"Mark a single TODO item as complete by ID. When the user wants "
"several items marked at once, call this tool in parallel — "
"one call per item — rather than sequentially across turns."
),
"parameters": {
"type": "object",
"properties": {
"todo_id": {
"type": "string",
"description": "Identifier of the TODO item",
},
"note": {
"type": "string",
"description": "Optional completion note",
},
},
"required": ["todo_id"],
},
},
},
]
_TODO_DB = {
"T-101": "Draft onboarding doc",
"T-102": "Update dependency lockfile",
"T-103": "Fix flaky login test",
"T-104": "Rotate service credentials",
"T-105": "Archive Q4 reports",
}
def _mark_todo_mock(args):
tid = args.get("todo_id", "")
if tid in _TODO_DB:
return json.dumps({"todo_id": tid, "title": _TODO_DB[tid], "status": "done"})
return json.dumps({"todo_id": tid, "error": "unknown id"})
TODO_BATCH_TEST = {
"name": "Batch TODO completion (same tool, 5 IDs in one turn)",
"tools": _TODO_TOOLS,
"messages": [
{
"role": "user",
"content": (
"I finished every item on today's list. Please mark all of the "
"following TODOs as complete, in one parallel batch: T-101, T-102, "
"T-103, T-104, T-105. Don't mark them one at a time across separate "
"turns — issue all five mark_todo_complete calls at once. Afterwards "
"confirm which ones succeeded."
),
}
],
"mock_tool_responses": {"mark_todo_complete": _mark_todo_mock},
"expected_parallel": {
"min_parallel": 5,
"require_same_tool": "mark_todo_complete",
"min_distinct_args_key": "todo_id",
"min_distinct_args_count": 5,
},
"validate": lambda turns, tcs, content: _validate_todo(turns, tcs, content),
}
def _validate_todo(turns, tcs, content):
del turns
if not content:
return False, "No confirmation summary produced"
return True, f"{len(tcs)} total mark_todo_complete calls"
# ---- Test 3: Multi-city weather (same tool, N parallel locations) ----
_WEATHER_TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": (
"Fetch current weather for ONE city. When the user asks about "
"several cities, call this tool in parallel — one call per city — "
"instead of sequentially."
),
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"default": "metric",
},
},
"required": ["city"],
},
},
},
]
_WEATHER_DB = {
"tokyo": {"city": "Tokyo", "temp_c": 18.4, "condition": "partly cloudy", "humidity": 64},
"london": {"city": "London", "temp_c": 9.1, "condition": "overcast", "humidity": 81},
"new york": {"city": "New York", "temp_c": 12.7, "condition": "clear", "humidity": 55},
"paris": {"city": "Paris", "temp_c": 11.3, "condition": "light rain", "humidity": 78},
}
def _weather_mock(args):
city = args.get("city", "").strip().lower()
if city.startswith("new york"):
city = "new york"
if city in _WEATHER_DB:
return json.dumps(_WEATHER_DB[city])
return json.dumps({"city": args.get("city", ""), "error": "unknown city"})
MULTI_WEATHER_TEST = {
"name": "Parallel multi-city weather (same tool, 4 cities)",
"tools": _WEATHER_TOOLS,
"messages": [
{
"role": "user",
"content": (
"I'm comparing today's weather across four cities for a travel "
"decision: Tokyo, London, New York, and Paris. Please call "
"get_weather for all four in parallel in a single turn — don't "
"fetch them one at a time. Then rank them from warmest to coolest."
),
}
],
"mock_tool_responses": {"get_weather": _weather_mock},
"expected_parallel": {
"min_parallel": 4,
"require_same_tool": "get_weather",
"min_distinct_args_key": "city",
"min_distinct_args_count": 4,
},
"validate": lambda turns, tcs, content: _validate_weather(turns, tcs, content),
}
def _validate_weather(turns, tcs, content):
del turns
if not content or not any(
kw in content.lower() for kw in ("warmest", "rank", "hot", "cool")
):
return False, f"Final content missing a ranking: {content!r}"
return True, f"{len(tcs)} total get_weather calls; ranking produced"
# ---- Test 4: Trip planning (different tools, parallel in one turn) ----
_TRIP_TOOLS = [
{
"type": "function",
"function": {
"name": "search_flights",
"description": "Search one-way flights between two airports on a given date.",
"parameters": {
"type": "object",
"properties": {
"from_airport": {"type": "string", "description": "IATA code, e.g. SFO"},
"to_airport": {"type": "string", "description": "IATA code, e.g. JFK"},
"date": {"type": "string", "description": "YYYY-MM-DD"},
},
"required": ["from_airport", "to_airport", "date"],
},
},
},
{
"type": "function",
"function": {
"name": "search_hotels",
"description": "Search hotels in a city for a date range.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"check_in": {"type": "string", "description": "YYYY-MM-DD"},
"check_out": {"type": "string", "description": "YYYY-MM-DD"},
"max_price": {"type": "integer"},
},
"required": ["city", "check_in", "check_out"],
},
},
},
{
"type": "function",
"function": {
"name": "search_restaurants",
"description": "Search restaurants in a city by cuisine.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"cuisine": {"type": "string"},
},
"required": ["city"],
},
},
},
]
_FLIGHTS_RESULT = {
"results": [
{"flight": "UA 1552", "depart": "08:15", "arrive": "16:45", "price": 389},
{"flight": "AA 20", "depart": "10:00", "arrive": "18:35", "price": 412},
]
}
_HOTELS_RESULT = {
"results": [
{"name": "Midtown Grand", "nightly_rate": 245, "rating": 4.3},
{"name": "Harbour Boutique", "nightly_rate": 312, "rating": 4.6},
]
}
_RESTAURANTS_RESULT = {
"results": [
{"name": "Trattoria Nona", "cuisine": "italian", "rating": 4.5},
{"name": "Osteria Blu", "cuisine": "italian", "rating": 4.4},
]
}
TRIP_PLAN_TEST = {
"name": "Trip planning (3 different tools in parallel)",
"tools": _TRIP_TOOLS,
"messages": [
{
"role": "user",
"content": (
"I'm flying from SFO to JFK on 2026-06-12 and staying four nights "
"(check out 2026-06-16). I'd also like some Italian restaurant "
"suggestions in New York. Please call search_flights, search_hotels, "
"and search_restaurants in parallel — all three in a single turn, "
"since they don't depend on each other. Then give me a concise "
"travel summary."
),
}
],
"mock_tool_responses": {
"search_flights": lambda _: json.dumps(_FLIGHTS_RESULT),
"search_hotels": lambda _: json.dumps(_HOTELS_RESULT),
"search_restaurants": lambda _: json.dumps(_RESTAURANTS_RESULT),
},
"expected_parallel": {
"min_parallel": 3,
"require_distinct_tools": 3,
},
"validate": lambda turns, tcs, content: _validate_trip(turns, tcs, content),
}
def _validate_trip(turns, tcs, content):
del turns
names = {tc["function"]["name"] for tc in tcs}
required = {"search_flights", "search_hotels", "search_restaurants"}
missing = required - names
if missing:
return False, f"Missing tool calls: {missing}"
if not content:
return False, "No travel summary produced"
return True, f"All three tools called; summary length={len(content)}"
# ---- Test 5: Portfolio check (same tool, parallel tickers) ----
_STOCK_TOOLS = [
{
"type": "function",
"function": {
"name": "get_stock_quote",
"description": (
"Get the latest quote for ONE ticker. When the user asks about "
"multiple tickers, call this tool in parallel — one per symbol — "
"rather than sequentially."
),
"parameters": {
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "Ticker symbol"},
},
"required": ["symbol"],
},
},
},
]
_STOCK_DB = {
"AAPL": {"symbol": "AAPL", "price": 218.45, "change_pct": "+0.8%"},
"MSFT": {"symbol": "MSFT", "price": 421.10, "change_pct": "+1.2%"},
"GOOGL":{"symbol": "GOOGL","price": 175.22, "change_pct": "-0.3%"},
"AMZN": {"symbol": "AMZN", "price": 189.76, "change_pct": "+0.5%"},
"NVDA": {"symbol": "NVDA", "price": 140.88, "change_pct": "+2.4%"},
}
def _stock_mock(args):
sym = args.get("symbol", "").strip().upper()
if sym in _STOCK_DB:
return json.dumps(_STOCK_DB[sym])
return json.dumps({"symbol": sym, "error": "unknown ticker"})
PORTFOLIO_TEST = {
"name": "Portfolio check (same tool, 5 tickers in parallel)",
"tools": _STOCK_TOOLS,
"messages": [
{
"role": "user",
"content": (
"Pull the latest quote for every ticker in my portfolio — AAPL, "
"MSFT, GOOGL, AMZN, and NVDA — in a single parallel batch. These "
"lookups are independent, so please don't chain them across turns. "
"Once you have all five, tell me which ticker had the biggest "
"percentage change today."
),
}
],
"mock_tool_responses": {"get_stock_quote": _stock_mock},
"expected_parallel": {
"min_parallel": 5,
"require_same_tool": "get_stock_quote",
"min_distinct_args_key": "symbol",
"min_distinct_args_count": 5,
},
"validate": lambda turns, tcs, content: _validate_portfolio(turns, tcs, content),
}
def _validate_portfolio(turns, tcs, content):
del turns
if not content or ("nvda" not in content.lower() and "NVDA" not in content):
return False, f"Expected NVDA to be identified as the biggest mover: {content!r}"
return True, f"{len(tcs)} total quotes pulled"
# ---- Test 6: Mixed — translate + dictionary in parallel for the same word ----
_LANG_TOOLS = [
{
"type": "function",
"function": {
"name": "translate_text",
"description": "Translate a short text into a target language.",
"parameters": {
"type": "object",
"properties": {
"text": {"type": "string"},
"target_language": {"type": "string",
"description": "ISO 639-1 language code, e.g. 'es'"},
},
"required": ["text", "target_language"],
},
},
},
{
"type": "function",
"function": {
"name": "get_definition",
"description": "Get the English dictionary definition of a word.",
"parameters": {
"type": "object",
"properties": {
"word": {"type": "string"},
},
"required": ["word"],
},
},
},
{
"type": "function",
"function": {
"name": "get_synonyms",
"description": "Get English synonyms for a word.",
"parameters": {
"type": "object",
"properties": {
"word": {"type": "string"},
},
"required": ["word"],
},
},
},
]
def _translate_mock(args):
t = args.get("text", "")
lang = args.get("target_language", "")
return json.dumps({"source": t, "target_language": lang, "translation": f"[{lang}] {t}"})
def _definition_mock(args):
w = args.get("word", "")
return json.dumps({
"word": w,
"definition": f"A standard dictionary definition of {w!r}.",
})
def _synonyms_mock(args):
w = args.get("word", "")
return json.dumps({
"word": w,
"synonyms": ["synonym_a", "synonym_b", "synonym_c"],
})
LANG_TOOLKIT_TEST = {
"name": "Language toolkit (translate + definition + synonyms in parallel)",
"tools": _LANG_TOOLS,
"messages": [
{
"role": "user",
"content": (
"For the English word 'resilient', I need three independent "
"look-ups at once: (a) translate it into Spanish, (b) fetch its "
"dictionary definition, and (c) list its synonyms. These three "
"calls don't depend on each other — please issue them in parallel "
"in a single turn. Then present the combined results as a short "
"language note."
),
}
],
"mock_tool_responses": {
"translate_text": _translate_mock,
"get_definition": _definition_mock,
"get_synonyms": _synonyms_mock,
},
"expected_parallel": {
"min_parallel": 3,
"require_distinct_tools": 3,
},
"validate": lambda turns, tcs, content: _validate_lang(turns, tcs, content),
}
def _validate_lang(turns, tcs, content):
del turns
names = {tc["function"]["name"] for tc in tcs}
required = {"translate_text", "get_definition", "get_synonyms"}
missing = required - names
if missing:
return False, f"Missing tool calls: {missing}"
if not content:
return False, "No language note produced"
return True, f"All three lookup tools called; note length={len(content)}"
# ---------------------------------------------------------------------------
# All test cases
# ---------------------------------------------------------------------------
ALL_TEST_CASES = [
MULTIFILE_READ_TEST,
TODO_BATCH_TEST,
MULTI_WEATHER_TEST,
TRIP_PLAN_TEST,
PORTFOLIO_TEST,
LANG_TOOLKIT_TEST,
]
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description=(
"Test llama-server parallel tool-calling capability. Run this only "
"against models configured for parallel tool calls — this script "
"does not configure that itself."
)
)
parser.add_argument("--host", default="localhost")
parser.add_argument("--port", default=9931, type=int)
parser.add_argument(
"--no-stream", action="store_true", help="Disable streaming mode tests"
)
parser.add_argument(
"--stream-only", action="store_true", help="Only run streaming mode tests"
)
parser.add_argument(
"--test",
help="Run only the test whose name contains this substring (case-insensitive)",
)
args = parser.parse_args()
url = f"http://{args.host}:{args.port}/v1/chat/completions"
print_info(f"Testing server at {url}")
print_warn(
"This script expects the target model to emit multiple tool calls in a "
"single assistant turn. Run it only against parallel-tool-capable models."
)
modes: list[bool] = []
if not args.stream_only:
modes.append(False)
if not args.no_stream:
modes.append(True)
cases: list[dict] = ALL_TEST_CASES
if args.test:
name_filter = args.test.lower()
cases = [c for c in cases if name_filter in str(c["name"]).lower()]
if not cases:
print_fail(f"No test cases matched '{args.test}'")
sys.exit(1)
total = 0
passed = 0
for stream in modes:
for case in cases:
total += 1
if run_test(url, case, stream=stream):
passed += 1
color = GREEN if passed == total else RED
_print(f"\n{BOLD}{color}{'─' * 60}{RESET}")
_print(f"{BOLD}{color} Results: {passed}/{total} passed{RESET}")
_print(f"{BOLD}{color}{'─' * 60}{RESET}\n")
sys.exit(0 if passed == total else 1)
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
0xffff
+426
View File
@@ -0,0 +1,426 @@
#!/usr/bin/env python3
#
# Build llama.cpp for Snapdragon (via Docker or natively) and push to device.
#
import sys
import os
import argparse
import subprocess
import platform
import shutil
import logging
import glob
import shlex
import tarfile
import tempfile
from sdk import validate_windows_sdks
logger = logging.getLogger("build")
def parse_target(target_str):
if not target_str:
return None, None, None
parts = target_str.split(":")
target_name = parts[0]
if target_name in ("adb", "android"):
serial = ":".join(parts[1:]) if len(parts) > 1 else None
return "android", serial, "adb"
elif target_name in ("lnx", "linux", "ubuntu"):
if len(parts) > 1 and parts[1] == "adb":
serial = ":".join(parts[2:]) if len(parts) > 2 else None
return "linux", serial, "adb"
host = ":".join(parts[1:]) if len(parts) > 1 else None
return "linux", host, "ssh"
elif target_str in ("wos", "windows"):
return "windows", None, None
else:
return None, None, None
def default_target_dir(target_type, target_transport, home_dir):
if target_type == "android":
return "/data/local/tmp/llama.cpp"
if target_type == "linux":
if home_dir:
return home_dir.rstrip("/") + "/llama.cpp"
if target_transport == "adb":
return "/home/llama.cpp"
return "~/llama.cpp"
return "~/llama.cpp"
def normalize_target_dir(target_dir):
target_dir = target_dir.rstrip("/")
if not target_dir or target_dir == "/":
raise RuntimeError("target directory must not be the filesystem root")
return target_dir
def adb_root(adb_cmd):
root_cmd = adb_cmd + ["root"]
logger.info(f"+ {' '.join(root_cmd)}")
res = subprocess.run(root_cmd)
if res.returncode != 0:
logger.error("ADB root failed.")
sys.exit(res.returncode)
wait_cmd = adb_cmd + ["wait-for-device"]
logger.info(f"+ {' '.join(wait_cmd)}")
res = subprocess.run(wait_cmd)
if res.returncode != 0:
logger.error("ADB wait-for-device failed.")
sys.exit(res.returncode)
def make_package_archive(src_path):
fd, archive_path = tempfile.mkstemp(prefix="llama-cpp-", suffix=".tar.gz")
os.close(fd)
with tarfile.open(archive_path, "w:gz", dereference=False) as tar:
for item in os.listdir(src_path):
if item.startswith("."):
continue
tar.add(os.path.join(src_path, item), arcname=item, recursive=True)
return archive_path
def get_uid_gid():
if platform.system() != "Windows":
return [f"{os.getuid()}:{os.getgid()}"]
return []
def main():
logging.basicConfig(level=logging.INFO, format='%(message)s')
parser = argparse.ArgumentParser(
description="Build llama.cpp for Snapdragon using cross-compilation docker containers or natively."
)
parser.add_argument("--target", default="android", help="Compilation target and deployment definition (e.g. android[:serial]/adb[:serial], ubuntu:adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, windows/wos) (default: android)")
parser.add_argument("--build-dir", help="Build directory name (defaults to build-TARGET[-dbg], e.g. build-android)")
parser.add_argument("--install-dir", help="Install directory name (defaults to pkg-TARGET[-dbg], e.g. pkg-android)")
parser.add_argument("--jobs", "-j", type=int, help="Number of build jobs (defaults to CPU thread count)")
parser.add_argument("--no-docker", action="store_true", help="Build natively on the host instead of in a docker container")
parser.add_argument("--preset", help="Override the CMake preset to use")
parser.add_argument("--debug", action="store_true", help="Build in debug mode (uses -debug presets instead of -release)")
parser.add_argument("--hex-only", "--hexagon-only", action="store_true", help="Only build and push ggml-hexagon and libggml-htp libraries")
parser.add_argument("--hex-arch", choices=["v73", "v75", "v79", "v81", "v85", "73", "75", "79", "81", "85"], help="Specific Hexagon DSP architecture to build with --hex-only (default: all)")
# Push options
parser.add_argument("--push", action="store_true", help="Push built package to the target device via ADB or SSH/SCP")
parser.add_argument("--target-dir", help="Target directory on the device (default: /data/local/tmp/llama.cpp for Android, ~/llama.cpp for Linux, /home/llama.cpp for Linux via ADB)")
parser.add_argument("--home-dir", help="Home directory for Linux targets when deriving the default target directory")
# Toolchain options
parser.add_argument("--toolchain-version", default="v0.8", help="Docker toolchain image version/tag (default: v0.8)")
parser.add_argument("--toolchain-url", default="ghcr.io/snapdragon-toolchain", help="Docker toolchain registry URL/namespace (default: ghcr.io/snapdragon-toolchain)")
args = parser.parse_args()
target_type, target_val, target_transport = parse_target(args.target)
if not target_type:
logger.error(f"Error: Invalid target format '{args.target}'. Must be android[:serial]/adb[:serial], ubuntu:adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, or windows/wos.")
sys.exit(1)
if target_type == "windows":
logger.info("Windows target selected. Forcing native compilation...")
args.no_docker = True
if platform.system() != "Windows":
logger.warning("Warning: Windows compilation is intended to run on Windows arm64 hosts.")
validate_windows_sdks()
# Determine preset and check if it's debug
preset = args.preset
if preset:
is_debug = args.debug or ("debug" in preset.lower())
else:
is_debug = args.debug
config_type = "debug" if is_debug else "release"
if args.no_docker:
if target_type == "windows" or platform.system() == "Windows":
preset = f"arm64-windows-snapdragon-{config_type}"
elif target_type == "linux":
preset = f"arm64-linux-snapdragon-{config_type}"
else:
preset = f"arm64-android-snapdragon-{config_type}"
else:
preset = f"arm64-linux-snapdragon-{config_type}" if target_type == "linux" else f"arm64-android-snapdragon-{config_type}"
target_prefix = args.target.split(":", 1)[0]
suffix = "-dbg" if is_debug else ""
build_dir = args.build_dir
if not build_dir:
build_dir = f"build-{target_prefix}{suffix}"
install_dir = args.install_dir
if not install_dir:
install_dir = f"pkg-{target_prefix}{suffix}"
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
# Ensure CMakeUserPresets.json is in the workspace root, update if docs version is newer
preset_src = os.path.join(repo_root, "docs", "backend", "snapdragon", "CMakeUserPresets.json")
preset_dst = os.path.join(repo_root, "CMakeUserPresets.json")
if os.path.exists(preset_src):
should_copy = False
if not os.path.exists(preset_dst):
should_copy = True
else:
# Check modification times
src_mtime = os.path.getmtime(preset_src)
dst_mtime = os.path.getmtime(preset_dst)
if src_mtime > dst_mtime:
preset_bak = preset_dst + ".bak"
logger.info(f"Docs CMakeUserPresets.json is newer. Backing up existing {preset_dst} to {preset_bak}")
shutil.copy2(preset_dst, preset_bak)
should_copy = True
if should_copy:
logger.info(f"Copying CMakeUserPresets.json from {preset_src} to {preset_dst}")
shutil.copy2(preset_src, preset_dst)
else:
logger.warning("Warning: CMakeUserPresets.json not found in docs/backend/snapdragon/.")
jobs = args.jobs if args.jobs else os.cpu_count() or 4
hex_arch = None
if args.hex_arch:
hex_arch = args.hex_arch if args.hex_arch.startswith("v") else f"v{args.hex_arch}"
if args.hex_only:
if hex_arch:
hex_targets = ["ggml-hexagon", f"htp-{hex_arch}"]
else:
hex_targets = ["ggml-hexagon", "htp-v73", "htp-v75", "htp-v79", "htp-v81", "htp-v85"]
if args.no_docker:
# Native/local host build
logger.info("Running native/local CMake build...")
install_prefix = os.path.join(repo_root, install_dir, "llama.cpp")
# Configure
configure_cmd = ["cmake", f"--preset={preset}", "-B", build_dir]
logger.info(f"+ {' '.join(configure_cmd)}")
res = subprocess.run(configure_cmd, cwd=repo_root)
if res.returncode != 0:
logger.error("CMake configuration failed.")
sys.exit(res.returncode)
# Build
build_cmd = ["cmake", "--build", build_dir]
if args.hex_only:
build_cmd += ["--target"] + hex_targets
build_cmd += ["-j", str(jobs)]
logger.info(f"+ {' '.join(build_cmd)}")
res = subprocess.run(build_cmd, cwd=repo_root)
if res.returncode != 0:
logger.error("CMake build failed.")
sys.exit(res.returncode)
# Install
install_cmd = ["cmake", "--install", build_dir, "--prefix", install_prefix]
logger.info(f"+ {' '.join(install_cmd)}")
res = subprocess.run(install_cmd, cwd=repo_root)
if res.returncode != 0:
logger.error("CMake install failed.")
sys.exit(res.returncode)
else:
# Docker-based build
logger.info("Running Docker-based cross-compilation build...")
image_name = "arm64-linux" if target_type == "linux" else "arm64-android"
image = f"{args.toolchain_url}/{image_name}:{args.toolchain_version}"
install_prefix_container = f"/workspace/{install_dir}/llama.cpp"
target_args = f"--target {' '.join(hex_targets)} " if args.hex_only else ""
build_sh_cmd = (
f"cmake --preset {preset} -B /workspace/{build_dir} && "
f"cmake --build /workspace/{build_dir} {target_args}-j {jobs} && "
f"cmake --install /workspace/{build_dir} --prefix {install_prefix_container}"
)
docker_cmd = [
"docker", "run", "--rm",
"--volume", f"{repo_root}:/workspace",
"--workdir", "/workspace",
"--platform", "linux/amd64"
]
uid_gid = get_uid_gid()
if uid_gid:
docker_cmd += ["-u", uid_gid[0]]
docker_cmd += [image, "bash", "-c", build_sh_cmd]
logger.info(f"+ {' '.join(docker_cmd)}")
res = subprocess.run(docker_cmd, cwd=repo_root)
if res.returncode != 0:
logger.error("Docker-based build failed.")
sys.exit(res.returncode)
logger.info("\nBuild and installation completed successfully!")
# Push/deploy if requested
if args.push:
src_path = os.path.join(repo_root, install_dir, "llama.cpp")
if not os.path.exists(src_path):
logger.error(f"Error: installation directory {src_path} does not exist. Cannot deploy.")
sys.exit(1)
# Resolve target directory on device
target_dir = args.target_dir
if not target_dir:
target_dir = default_target_dir(target_type, target_transport, args.home_dir)
target_dir = normalize_target_dir(target_dir)
sub_items = [item for item in os.listdir(src_path) if not item.startswith(".")]
if args.hex_only:
lib_dir = os.path.join(src_path, "lib")
hex_libs = glob.glob(os.path.join(lib_dir, "libggml-hexagon*"))
if hex_arch:
hex_libs += glob.glob(os.path.join(lib_dir, f"libggml-htp-{hex_arch}*"))
else:
hex_libs += glob.glob(os.path.join(lib_dir, "libggml-htp*"))
if not hex_libs:
logger.error(f"Error: no hexagon libraries found in {lib_dir} to push.")
sys.exit(1)
if target_transport == "adb":
adb_cmd = ["adb"]
if target_val: # serial
adb_cmd += ["-s", target_val]
if target_type == "linux":
adb_root(adb_cmd)
if args.hex_only:
logger.info("\nPushing Hexagon libraries to target device via ADB...")
target_lib_dir = f"{target_dir}/lib"
mkdir_cmd = adb_cmd + ["shell", f"mkdir -p {shlex.quote(target_lib_dir)}"]
logger.info(f"+ {' '.join(mkdir_cmd)}")
res = subprocess.run(mkdir_cmd)
if res.returncode != 0:
logger.error("ADB mkdir failed.")
sys.exit(res.returncode)
for lib in hex_libs:
push_cmd = adb_cmd + ["push", lib, f"{target_lib_dir}/"]
logger.info(f"+ {' '.join(push_cmd)}")
res = subprocess.run(push_cmd)
if res.returncode != 0:
logger.error("ADB push failed.")
sys.exit(res.returncode)
logger.info("Hexagon libraries deployed successfully!")
elif target_type == "linux":
logger.info("\nDeploying Linux package to target device via ADB tar...")
remote_archive = f"/tmp/llama.cpp-package-{os.getpid()}.tar.gz"
archive_path = make_package_archive(src_path)
try:
mkdir_cmd = adb_cmd + ["shell", f"mkdir -p {shlex.quote(target_dir)}"]
logger.info(f"+ {' '.join(mkdir_cmd)}")
res = subprocess.run(mkdir_cmd)
if res.returncode != 0:
logger.error("ADB mkdir failed.")
sys.exit(res.returncode)
if sub_items:
clean_paths = " ".join(shlex.quote(f"{target_dir}/{item}") for item in sub_items)
clean_cmd = adb_cmd + ["shell", f"rm -rf {clean_paths}"]
logger.info(f"+ {' '.join(clean_cmd)}")
subprocess.run(clean_cmd)
push_cmd = adb_cmd + ["push", archive_path, remote_archive]
logger.info(f"+ {' '.join(push_cmd)}")
res = subprocess.run(push_cmd)
if res.returncode != 0:
logger.error("ADB push failed.")
sys.exit(res.returncode)
extract_cmd = adb_cmd + ["shell", f"tar -xzf {shlex.quote(remote_archive)} -C {shlex.quote(target_dir)} && rm -f {shlex.quote(remote_archive)}"]
logger.info(f"+ {' '.join(extract_cmd)}")
res = subprocess.run(extract_cmd)
if res.returncode != 0:
logger.error("ADB untar failed.")
sys.exit(res.returncode)
chmod_cmd = adb_cmd + ["shell", f"chmod -R 755 {shlex.quote(target_dir)}/bin 2>/dev/null || true"]
logger.info(f"+ {' '.join(chmod_cmd)}")
subprocess.run(chmod_cmd)
finally:
os.unlink(archive_path)
logger.info("ADB tar deploy completed successfully!")
else:
# Clean stale package files on device
if sub_items:
clean_paths = " ".join(f"{target_dir}/{item}" for item in sub_items)
clean_cmd = adb_cmd + ["shell", f"rm -rf {clean_paths}"]
logger.info(f"+ {' '.join(clean_cmd)}")
subprocess.run(clean_cmd)
# Android destination directory is target_dir
push_cmd = adb_cmd + ["push", os.path.join(src_path, "."), target_dir]
logger.info(f"+ {' '.join(push_cmd)}")
res = subprocess.run(push_cmd)
if res.returncode != 0:
logger.error("ADB push failed.")
sys.exit(res.returncode)
chmod_cmd = adb_cmd + ["shell", f"chmod -R 755 {target_dir}/bin 2>/dev/null || true"]
logger.info(f"+ {' '.join(chmod_cmd)}")
subprocess.run(chmod_cmd)
logger.info("ADB push completed successfully!")
elif target_type == "linux":
ssh_host = target_val
if not ssh_host:
logger.error("Error: SSH host not specified in target (e.g. use linux:user@host, lnx:user@host, or ubuntu:user@host). Cannot deploy.")
sys.exit(1)
if args.hex_only:
logger.info(f"\nDeploying Hexagon libraries to Linux device {ssh_host} via SCP...")
target_lib_dir = f"{target_dir}/lib"
deploy_cmd = ["scp"] + hex_libs + [f"{ssh_host}:{target_lib_dir}/"]
logger.info(f"+ {' '.join(deploy_cmd)}")
res = subprocess.run(deploy_cmd)
if res.returncode != 0:
logger.error("SCP deploy failed.")
sys.exit(res.returncode)
logger.info("Hexagon libraries deployed successfully!")
else:
# Clean stale package files on remote host
if sub_items:
clean_paths = " ".join(f"{target_dir}/{item}" for item in sub_items)
clean_cmd = ["ssh", ssh_host, f"rm -rf {clean_paths}"]
logger.info(f"+ {' '.join(clean_cmd)}")
subprocess.run(clean_cmd)
# Deploy to target_dir
deploy_cmd = ["scp", "-r", os.path.join(src_path, "."), f"{ssh_host}:{target_dir}"]
logger.info(f"+ {' '.join(deploy_cmd)}")
res = subprocess.run(deploy_cmd)
if res.returncode != 0:
logger.error("SSH/SCP deploy failed.")
sys.exit(res.returncode)
chmod_cmd = ["ssh", ssh_host, f"chmod -R 755 {target_dir}/bin 2>/dev/null || true"]
logger.info(f"+ {' '.join(chmod_cmd)}")
subprocess.run(chmod_cmd)
logger.info("SSH/SCP deploy completed successfully!")
elif target_type == "windows":
logger.info("\nPush for Windows on Snapdragon (windows) target is currently a stub.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("\nInterrupted by user.")
sys.exit(130)
except RuntimeError as err:
logger.error("Error: %s", err)
sys.exit(1)
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""
align-macros.py - Inspect and align trailing backslashes in multiline C/C++ macros.
Usage:
align-macros.py [paths...] # Check and report misaligned macros
align-macros.py --diff [paths...] # Show unified diff of fixes
align-macros.py --fix [paths...] # Fix misaligned macros in-place
align-macros.py --fix --mode majority ... # Align to the dominant column
align-macros.py --fix --pad 2 ... # Align to (max_content_len + pad)
Safety rules:
- Macros that are ALREADY aligned are NEVER touched (unless --all is given).
- Whitespace after trailing backslashes is flagged and cleaned.
"""
import argparse
import difflib
import logging
import os
import re
import sys
from collections import Counter
from typing import List, Optional, Tuple, NamedTuple
logger = logging.getLogger("ggml-hexagon-align-macros")
class MacroLine(NamedTuple):
line_num: int # 1-indexed
raw: str # Original line including newline
content: str # Line content before trailing backslash (stripped of trailing whitespace)
bs_col: Optional[int] # 1-indexed column of backslash, or None if last line has no backslash
trailing_ws: bool # True if whitespace existed after the backslash
class MacroDef(NamedTuple):
name: str
filepath: str
start_line: int
end_line: int
lines: List[MacroLine]
def parse_macros(filepath: str) -> List[MacroDef]:
"""Extract all multiline macros from a C/C++ source file."""
try:
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
except Exception as e:
logger.error(f"Error reading {filepath}: {e}")
return []
macros: List[MacroDef] = []
i = 0
n = len(lines)
while i < n:
line = lines[i]
m = re.match(r"^\s*#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)", line)
if m:
macro_name = m.group(1)
macro_start = i + 1
macro_lines: List[MacroLine] = []
cur = i
while cur < n:
l_raw = lines[cur]
l_rstrip = l_raw.rstrip("\r\n")
# Check if line has a trailing backslash
# Note: handle possible accidental spaces after backslash
match_bs = re.search(r"\\([ \t]*)$", l_rstrip)
if match_bs:
has_trailing_ws = len(match_bs.group(1)) > 0
bs_index = match_bs.start()
content = l_rstrip[:bs_index].rstrip()
# 1-indexed column of the backslash
bs_col = bs_index + 1
macro_lines.append(MacroLine(
line_num=cur + 1,
raw=l_raw,
content=content,
bs_col=bs_col,
trailing_ws=has_trailing_ws
))
cur += 1
else:
# Line does not end with backslash
if cur == i:
# Single-line macro, not multiline
break
else:
# Final line of a multiline macro
macro_lines.append(MacroLine(
line_num=cur + 1,
raw=l_raw,
content=l_rstrip.rstrip(),
bs_col=None,
trailing_ws=False
))
break
# Only record if it is a multiline macro (has at least one continuation line)
continuation_lines = [ml for ml in macro_lines if ml.bs_col is not None]
if continuation_lines:
macro_end = macro_lines[-1].line_num
macros.append(MacroDef(
name=macro_name,
filepath=filepath,
start_line=macro_start,
end_line=macro_end,
lines=macro_lines
))
i = cur
i += 1
return macros
def is_macro_aligned(macro: MacroDef) -> bool:
"""A macro is aligned if all continuation lines have backslashes at the same column."""
bs_cols = [ml.bs_col for ml in macro.lines if ml.bs_col is not None]
if not bs_cols:
return True
has_trailing_ws = any(ml.trailing_ws for ml in macro.lines)
return len(set(bs_cols)) == 1 and not has_trailing_ws
def compute_target_column(macro: MacroDef, mode: str, pad: int, target_col: Optional[int]) -> int:
"""Determine the column where backslashes should be aligned."""
max_content_len = max(len(ml.content) for ml in macro.lines)
min_needed = max_content_len + pad
if target_col is not None:
return max(target_col, min_needed)
bs_cols = [ml.bs_col for ml in macro.lines if ml.bs_col is not None]
if not bs_cols:
return min_needed
if mode == "min":
return min_needed
elif mode == "max":
return max(max(bs_cols), min_needed)
elif mode == "majority":
counts = Counter(bs_cols)
# Sort by frequency descending, then by column descending
majority_col = sorted(counts.items(), key=lambda x: (-x[1], -x[0]))[0][0]
return max(majority_col, min_needed)
else:
return min_needed
def realign_macro_lines(macro: MacroDef, target_col: int) -> List[str]:
"""Format macro lines with backslashes aligned at target_col."""
new_lines: List[str] = []
for ml in macro.lines:
nl = "\r\n" if ml.raw.endswith("\r\n") else "\n"
if ml.bs_col is None:
# Last line without backslash
new_lines.append(ml.raw)
else:
if not ml.content:
spaces = " " * (target_col - 1)
new_lines.append(f"{spaces}\\{nl}")
else:
spaces_needed = max(1, target_col - len(ml.content) - 1)
new_lines.append(f"{ml.content}{' ' * spaces_needed}\\{nl}")
return new_lines
def process_file(filepath: str, args: argparse.Namespace) -> Tuple[int, int, Optional[str]]:
macros = parse_macros(filepath)
if not macros:
return 0, 0, None
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
file_lines = f.readlines()
misaligned_count = 0
modified = False
new_file_lines = list(file_lines)
for macro in macros:
aligned = is_macro_aligned(macro)
if not aligned or args.all:
if not aligned:
misaligned_count += 1
bs_cols = [ml.bs_col for ml in macro.lines if ml.bs_col is not None]
max_content = max(len(ml.content) for ml in macro.lines)
col_counts = Counter(bs_cols)
if not args.quiet:
logger.info(f"{filepath}:{macro.start_line}-{macro.end_line} [{macro.name}]")
logger.info(f" Max content width: {max_content}, Min needed column (+{args.pad}): {max_content + args.pad}")
logger.info(f" Current backslash columns: {dict(sorted(col_counts.items()))}")
trailing_ws_lines = [ml.line_num for ml in macro.lines if ml.trailing_ws]
if trailing_ws_lines:
logger.warning(f" Warning: Trailing whitespace after backslash on line(s): {trailing_ws_lines}")
target_col = compute_target_column(macro, args.mode, args.pad, args.target_col)
if not args.quiet:
logger.info(f" -> Target alignment column: {target_col}")
realigned = realign_macro_lines(macro, target_col)
start_idx = macro.start_line - 1
end_idx = start_idx + len(macro.lines)
if new_file_lines[start_idx:end_idx] != realigned:
new_file_lines[start_idx:end_idx] = realigned
modified = True
diff_text = None
if modified:
diff = difflib.unified_diff(
file_lines,
new_file_lines,
fromfile=f"a/{filepath}",
tofile=f"b/{filepath}",
lineterm=""
)
diff_text = "\n".join(diff)
if args.fix:
with open(filepath, "w", encoding="utf-8") as f:
f.writelines(new_file_lines)
if not args.quiet:
logger.info(f" [FIXED] Updated {filepath}")
return len(macros), misaligned_count, diff_text
def find_source_files(paths: List[str]) -> List[str]:
extensions = {".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".inl"}
result: List[str] = []
for p in paths:
if os.path.isfile(p):
result.append(p)
elif os.path.isdir(p):
for root, _, files in os.walk(p):
for file in sorted(files):
_, ext = os.path.splitext(file)
if ext.lower() in extensions:
result.append(os.path.join(root, file))
return sorted(result)
def main():
logging.basicConfig(level=logging.INFO, format="%(message)s")
parser = argparse.ArgumentParser(
description="Inspect and align backslashes in multiline C/C++ macros."
)
parser.add_argument("paths", nargs="*", default=["."], help="Files or directories to scan (default: current dir)")
parser.add_argument("--fix", action="store_true", help="Fix misaligned macros in-place")
parser.add_argument("--diff", action="store_true", help="Display unified diff of suggested fixes")
parser.add_argument("--check", action="store_true", help="Exit with code 1 if misaligned macros exist")
parser.add_argument("--mode", choices=["min", "max", "majority"], default="min",
help="Alignment mode: 'min' (max_len + pad), 'max' (max existing col), 'majority' (dominant col)")
parser.add_argument("--pad", type=int, default=2, help="Spaces between longest line and backslash (default: 2)")
parser.add_argument("--target-col", type=int, default=None, help="Force alignment to an exact column")
parser.add_argument("--all", action="store_true", help="Realign all macros even if already aligned (default: only misaligned)")
parser.add_argument("-q", "--quiet", action="store_true", help="Only output errors and diffs/summary")
args = parser.parse_args()
files = find_source_files(args.paths)
if not files:
logger.error("No C/C++ source files found.")
sys.exit(0)
total_macros = 0
total_misaligned = 0
diffs: List[str] = []
for filepath in files:
num_macros, num_misaligned, diff_text = process_file(filepath, args)
total_macros += num_macros
total_misaligned += num_misaligned
if diff_text:
diffs.append(diff_text)
if args.diff and diffs:
logger.info("\n--- Proposed Changes ---\n")
for d in diffs:
logger.info(d)
logger.info(f"\nSummary: scanned {len(files)} files, {total_macros} multiline macros, {total_misaligned} misaligned.")
if args.check and total_misaligned > 0:
sys.exit(1)
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
+901
View File
@@ -0,0 +1,901 @@
#!/usr/bin/env python3
import sys
import os
import re
import argparse
import statistics
import logging
import bisect
from typing import Any, Dict, List, Optional, Iterable
from collections import defaultdict
# Mapping of cli-friendly names to (internal_data_key, Display Header, numeric_sort_key)
COL_MAP = {
"tot-usec": ("tot_usec", "Tot usec", "_sort_tot_usec"),
"op": ("op", "Op", "op"),
"dims": ("dims", "Dims", "dims"),
"dtypes": ("dtypes", "DTypes", "dtypes"),
"count": ("count", "Count", "_sort_count"),
"max-usec": ("max_usec", "Max usec", "_sort_max_usec"),
"avg-usec": ("avg_usec", "Avg usec", "_sort_avg_usec"),
"max-cycles": ("max_cycles", "Max Cycles", "_sort_max_cycles"),
"avg-cycles": ("avg_cycles", "Avg Cycles", "_sort_avg_cycles"),
"max-pmu": ("max_pmu", "Max PMU", "_sort_max_pmu"),
"avg-pmu": ("avg_pmu", "Avg PMU", "_sort_avg_pmu"),
}
op_pattern = re.compile(
r"profile-op\s+(?P<op_name>[A-Z_0-9+]+):\s+.*?\s+:\s+(?P<dims>[\d:x\s\->!]+)\s+:\s+(?P<types>[a-z\d_\s\->x]+)\s+:\s+.*?\s+:\s+(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?"
)
trace_pattern = re.compile(
r"trace-evt\s+(?P<event>[A-Z_0-9\-]+):\s+thread\s+(?P<thread>\d+)\s+info\s+(?P<info>\d+)\s+(?P<state>start|stop)\s+(?P<cycles>\d+)"
)
device_pattern = re.compile(r"\b(HTP\d+(?::\d+)?)\s+(?:profile-op|trace-evt)\b")
def extract_device(line):
m = device_pattern.search(line)
if m:
return m.group(1)
return "HTP0"
def device_matches(record_device, target_device):
targets = [t.strip() for t in target_device.split(',')]
for target in targets:
if record_device == target:
return True
if record_device.startswith(target + ":"):
return True
return False
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
logger = logging.getLogger("ggml-hexagon-profile")
def normalize_event_name(evt_type):
if evt_type == "HVX_COMP":
return "V-COMP"
if evt_type == "HMX_COMP":
return "M-COMP"
# Strip HVX_ or HMX_ prefixes
name = evt_type
if name.startswith("HVX_") or name.startswith("HMX_"):
name = name[4:]
return name.replace("_", "-")
class CycleUnwrapper:
def __init__(self, initial_val=None):
if initial_val is not None:
self.last_raw = initial_val & 0xFFFFFFFF
self.high_part = initial_val & 0xFFFFFFFF00000000
else:
self.last_raw = None
self.high_part = 0
def unwrap(self, raw):
if self.last_raw is None:
self.last_raw = raw
return raw
diff = raw - self.last_raw
if diff < -0x80000000:
self.high_part += 0x100000000
elif diff > 0x80000000:
self.high_part -= 0x100000000
self.last_raw = raw
return raw + self.high_part
def parse_log(file_path, pmu_index=None, limit=None, device_filter=None, op_filter_re=None):
try:
if file_path != "-":
f = open(file_path, 'r', encoding='utf-8', errors='ignore')
else:
f = os.fdopen(0, 'r', encoding='utf-8', errors='ignore')
except FileNotFoundError:
logger.error(f"file '{file_path}' not found.")
sys.exit(1)
all_ops: List[Dict[str, Any]] = []
all_traces: List[Dict[str, Any]] = []
current_op: Optional[Dict[str, Any]] = None
ops_count_per_device = {}
if device_filter is not None:
for target in device_filter.split(','):
ops_count_per_device[target.strip()] = 0
limit_reached = False
timestamp_pattern = re.compile(r"(?P<min>\d+)\.(?P<sec>\d+)\.(?P<ms>\d+)\.(?P<us>\d+)\s+[A-Z]\s+")
unwrappers = {}
last_batch_start = {}
trace_unwrappers = {}
for line in f:
if "profile-op" not in line and "trace-evt" not in line:
continue
ts_match = timestamp_pattern.search(line)
abs_usec = 0
if ts_match:
abs_usec = (
(int(ts_match.group('min')) * 60 + int(ts_match.group('sec'))) * 1000000
+ int(ts_match.group('ms')) * 1000
+ int(ts_match.group('us'))
)
device = extract_device(line)
idx = line.find("profile-op")
if idx != -1 and "|" in line[idx:]:
parts = [p.strip() for p in line[idx:].split("|")]
prefix = parts[0]
prefix_match = re.search(r"profile-op\s+(?P<op_name>[A-Z_0-9+]+)", prefix)
if not prefix_match:
continue
names = parts[1]
if len(parts) == 7:
dims, types, timings = parts[2], parts[3], parts[6]
elif len(parts) == 6:
dims, types, timings = parts[2], parts[3], parts[5]
else:
continue
timing_match = re.search(
r"(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?",
timings
)
if not timing_match:
continue
op_match = timing_match
op_name = prefix_match.group("op_name")
else:
op_match = op_pattern.search(line)
if op_match:
op_name = op_match.group('op_name')
names = ""
dims = op_match.group('dims').strip()
types = op_match.group('types').strip()
else:
op_match = None
if op_match:
pmu_raw = op_match.group('pmu') if 'pmu' in op_match.groupdict() else None
pmu_val = None
if pmu_raw and pmu_index is not None:
try:
pmu_list = [int(x.strip()) for x in pmu_raw.split(',')]
if len(pmu_list) > pmu_index:
pmu_val = pmu_list[pmu_index]
except (ValueError, IndexError):
pmu_val = None
evt_val = None
if types.startswith("evt-cnt "):
try:
evt_val = [int(x.strip()) for x in types[8:].split(',')]
except ValueError:
evt_val = None
cycles_start_raw = op_match.group('start')
unwrapped_cycles_start = None
if op_name == "OPBATCH":
if cycles_start_raw:
unwrapped_cycles_start = int(cycles_start_raw)
unwrappers[device] = CycleUnwrapper(unwrapped_cycles_start)
last_batch_start[device] = unwrapped_cycles_start
for k in list(trace_unwrappers.keys()):
if k[0] == device:
del trace_unwrappers[k]
else:
if cycles_start_raw:
device_unwrapper = unwrappers.get(device)
if device_unwrapper is not None:
unwrapped_cycles_start = device_unwrapper.unwrap(int(cycles_start_raw))
op_text = re.sub(r"^profile-op\s+", "", line[idx:]).strip() if idx != -1 else line.strip()
current_op = {
'name': op_name,
'names': names,
'dims': dims,
'types': types,
'op_text': op_text,
'usec': int(op_match.group('usec')),
'cycles': int(op_match.group('cycles')),
'cycles_start': int(cycles_start_raw) if cycles_start_raw else None,
'unwrapped_cycles_start': unwrapped_cycles_start,
'pmu_val': pmu_val,
'evt_val': evt_val,
'mhz': float(op_match.group('mhz')) if op_match.group('mhz') else None,
'abs_usec': abs_usec,
'trace_events': [],
'device': device
}
all_ops.append(current_op)
# Check if matching early exit criteria
matched = False
matched_target = None
if device_filter is not None:
targets = [t.strip() for t in device_filter.split(',')]
for target in targets:
if device == target or device.startswith(target + ":"):
matched = True
matched_target = target
break
else:
matched = True
matched_target = device
if op_filter_re is not None and not op_filter_re.search(op_text):
matched = False
if matched:
if matched_target not in ops_count_per_device:
ops_count_per_device[matched_target] = 0
ops_count_per_device[matched_target] += 1
if limit is not None and len(ops_count_per_device) > 0 and all(count >= limit for count in ops_count_per_device.values()):
limit_reached = True
if limit_reached and op_name == "OPBATCH":
break
continue
trace_match = trace_pattern.search(line)
if trace_match:
thread = int(trace_match.group('thread'))
raw_cyc = int(trace_match.group('cycles'))
unwrapped_cyc = None
th_key = (device, thread)
if th_key not in trace_unwrappers:
batch_start = last_batch_start.get(device)
trace_unwrappers[th_key] = CycleUnwrapper(batch_start)
unwrapped_cyc = trace_unwrappers[th_key].unwrap(raw_cyc)
all_traces.append({
'thread': thread,
'event': trace_match.group('event'),
'info': int(trace_match.group('info')),
'cycles': raw_cyc,
'unwrapped_cycles': unwrapped_cyc,
'state': trace_match.group('state'),
'device': device
})
f.close()
# Assign start/end cycles to all ops
for op in all_ops:
op['start_cycles'] = op['unwrapped_cycles_start']
op['end_cycles'] = op['start_cycles'] + op['cycles'] if op['start_cycles'] is not None else None
# Group ops by device
valid_ops_by_dev = defaultdict(list)
for op in all_ops:
if op['start_cycles'] is not None and op['end_cycles'] is not None:
valid_ops_by_dev[op['device']].append(op)
# Group trace events by device
traces_by_dev = defaultdict(list)
for e in all_traces:
if e['unwrapped_cycles'] is not None:
traces_by_dev[e['device']].append(e)
for device, dev_ops in valid_ops_by_dev.items():
opbatch_ops = [op for op in dev_ops if op['name'] == "OPBATCH"]
other_ops = [op for op in dev_ops if op['name'] != "OPBATCH"]
opbatch_ops.sort(key=lambda op: op['start_cycles'])
other_ops.sort(key=lambda op: op['start_cycles'])
opbatch_starts = [op['start_cycles'] for op in opbatch_ops]
other_starts = [op['start_cycles'] for op in other_ops]
dev_traces = traces_by_dev.get(device, [])
for e in dev_traces:
cyc = e['unwrapped_cycles']
# Map to OPBATCH
idx = bisect.bisect_right(opbatch_starts, cyc) - 1
if idx >= 0:
op = opbatch_ops[idx]
if op['start_cycles'] <= cyc <= op['end_cycles']:
op['trace_events'].append(e)
# Map to other ops
idx = bisect.bisect_right(other_starts, cyc) - 1
if idx >= 0:
op = other_ops[idx]
if op['start_cycles'] <= cyc <= op['end_cycles']:
op['trace_events'].append(e)
return all_ops
def print_bubbles_timeline(op):
op_name = op['name']
dims = op['dims']
types = op['types']
usec = op['usec']
cycles = op['cycles']
events = op['trace_events']
logger.info("=" * 100)
logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles")
logger.info("=" * 100)
if not events:
logger.info(" No trace events recorded.")
return
# Identify start and end cycles for this operator
op_start = op['start_cycles']
op_end = op['end_cycles']
if op_start is None or op_end is None:
logger.info(" Cannot analyze bubbles: missing start/end cycle counts.")
return
batch_duration = op_end - op_start
if batch_duration <= 0:
logger.info(" Cannot analyze bubbles: batch duration is 0.")
return
# Group events by (thread, track_type)
tracks = defaultdict(list)
for e in events:
t = e['thread']
is_dma = (normalize_event_name(e['event']) == 'DMA')
track_type = 'dma' if is_dma else 'compute'
tracks[(t, track_type)].append(e)
active_threads = sorted(list(set(t for (t, track_type) in tracks.keys())))
if not active_threads:
logger.info(" No active threads in trace.")
return
bubble_threshold = 10000 # 10k cycles
thread_stats = {}
for t in active_threads:
thread_stats[t] = {
'compute_idle_cycles': batch_duration,
'compute_idle_pct': 100.0,
'compute_bubbles': [],
'dma_idle_cycles': batch_duration,
'dma_idle_pct': 100.0,
'dma_bubbles': []
}
total_compute_idle_pct = 0.0
total_dma_idle_pct = 0.0
for t in active_threads:
for track_type in ['compute', 'dma']:
key = (t, track_type)
track_events = tracks.get(key, [])
if not track_events:
gaps = [(op_start, op_end)]
idle_cycles = batch_duration
else:
track_events = sorted(track_events, key=lambda e: e.get('unwrapped_cycles') or e['cycles'])
active_intervals = []
active_count = 0
curr_start = None
for e in track_events:
cyc = e.get('unwrapped_cycles') or e['cycles']
cyc = max(op_start, min(op_end, cyc))
state = e['state']
if state == 'start':
if active_count == 0:
curr_start = cyc
active_count += 1
elif state == 'stop':
if active_count > 0:
active_count -= 1
if active_count == 0:
active_intervals.append((curr_start, cyc))
else:
active_intervals.append((op_start, cyc))
if active_count > 0 and curr_start is not None:
active_intervals.append((curr_start, op_end))
# Merge intervals
active_intervals.sort(key=lambda x: x[0])
merged_intervals = []
for start, end in active_intervals:
if not merged_intervals:
merged_intervals.append([start, end])
else:
last_start, last_end = merged_intervals[-1]
if start <= last_end:
merged_intervals[-1][1] = max(last_end, end)
else:
merged_intervals.append([start, end])
# Calculate gaps
gaps = []
curr_time = op_start
for start, end in merged_intervals:
if start > curr_time:
gaps.append((curr_time, start))
curr_time = max(curr_time, end)
if curr_time < op_end:
gaps.append((curr_time, op_end))
idle_cycles = sum(end - start for start, end in gaps)
idle_pct = (idle_cycles / batch_duration) * 100.0
bubbles = []
for start, end in gaps:
dur = end - start
if dur >= bubble_threshold:
bubbles.append((start, end, dur))
if track_type == 'compute':
thread_stats[t]['compute_idle_cycles'] = idle_cycles
thread_stats[t]['compute_idle_pct'] = idle_pct
thread_stats[t]['compute_bubbles'] = bubbles
total_compute_idle_pct += idle_pct
else:
thread_stats[t]['dma_idle_cycles'] = idle_cycles
thread_stats[t]['dma_idle_pct'] = idle_pct
thread_stats[t]['dma_bubbles'] = bubbles
total_dma_idle_pct += idle_pct
avg_compute_idle = total_compute_idle_pct / len(active_threads)
avg_dma_idle = total_dma_idle_pct / len(active_threads)
logger.info(" Combined Idle Statistics:")
logger.info(f" Active Threads : {', '.join(str(t) for t in active_threads)}")
logger.info(f" Avg Thread Compute IDLE : {avg_compute_idle:.1f}%")
logger.info(f" Avg Thread DMA IDLE : {avg_dma_idle:.1f}%")
logger.info("-" * 100)
logger.info(" Per-Thread Idle Analysis:")
for t in active_threads:
stats = thread_stats[t]
thread_name = f"Thread {t:<2} (HVX)" if t != 10 else "Thread 10 (HMX)"
logger.info(f" {thread_name} -> Compute Idle: {stats['compute_idle_pct']:.1f}% | DMA Idle: {stats['dma_idle_pct']:.1f}%")
all_bubbles = []
for t in active_threads:
stats = thread_stats[t]
assert isinstance(stats['dma_bubbles'], Iterable)
assert isinstance(stats['compute_bubbles'], Iterable)
for start, end, dur in stats['compute_bubbles']:
pct = (dur / batch_duration) * 100.0
all_bubbles.append((dur, f"Thread {t} Compute: bubble of {dur} cycles ({pct:.1f}%) at {start - op_start} to {end - op_start}"))
for start, end, dur in stats['dma_bubbles']:
pct = (dur / batch_duration) * 100.0
all_bubbles.append((dur, f"Thread {t} DMA : bubble of {dur} cycles ({pct:.1f}%) at {start - op_start} to {end - op_start}"))
if all_bubbles:
logger.info("-" * 100)
logger.info(f" Significant Bubbles (>= {bubble_threshold} cycles):")
all_bubbles.sort(key=lambda x: x[0], reverse=True)
for dur, desc in all_bubbles[:15]:
logger.info(f" {desc}")
else:
logger.info("-" * 100)
logger.info(f" No significant bubbles detected (all idle gaps < {bubble_threshold} cycles).")
def print_ascii_summary(op_name, dims, types, usec, cycles, events):
logger.info("=" * 100)
logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles")
logger.info("=" * 100)
events = sorted(events, key=lambda e: e['cycles'])
if not events:
logger.info(" No trace events recorded.")
return
active_starts = {}
thread_totals = defaultdict(lambda: defaultdict(int))
for e in events:
t = e['thread']
evt = e['event']
info = e['info']
cyc = e['cycles']
state = e['state']
key = (t, evt, info)
if state == 'start':
active_starts[key] = cyc
elif state == 'stop':
if key in active_starts:
start_cyc = active_starts[key]
del active_starts[key]
if cyc >= start_cyc:
dur = cyc - start_cyc
else:
dur = (cyc + 0x100000000) - start_cyc
norm_evt = normalize_event_name(evt)
thread_totals[t][norm_evt] += dur
for t in sorted(thread_totals.keys()):
thread_name = f"Thread {t} (HVX)" if t != 10 else "Thread 10 (HMX)"
sorted_evts = sorted(thread_totals[t].items(), key=lambda item: item[0])
evt_strs = []
for evt, dur in sorted_evts:
pct = (dur / cycles * 100) if cycles > 0 else 0
evt_strs.append(f"{evt} {dur} ({pct:.1f}%)")
logger.info(f" {thread_name:<16}: " + " | ".join(evt_strs))
def generate_report(ops, top_n, width_overrides, sort_col, pmu_name=None):
if not ops:
logger.info("No valid records found.")
return
grouped = defaultdict(list)
for op in ops:
key = (op['name'], op['dims'], op['types'])
grouped[key].append(op)
group_stats = []
for (name, dims, types), group_ops in grouped.items():
usecs = [o['usec'] for o in group_ops]
cycles = [o['cycles'] for o in group_ops]
pmu_vals = [o['pmu_val'] for o in group_ops if o['pmu_val'] is not None]
avg_usec_val = statistics.mean(usecs)
count_val = len(group_ops)
tot_usec_val = avg_usec_val * count_val
group_stats.append({
'op': name,
'dims': dims,
'dtypes': types,
'count': str(count_val),
'max_usec': str(max(usecs)),
'avg_usec': f"{avg_usec_val:.2f}",
'tot_usec': f"{tot_usec_val:.2f}",
'max_cycles': str(max(cycles)),
'avg_cycles': f"{statistics.mean(cycles):.2f}",
'max_pmu': str(max(pmu_vals)) if pmu_vals else "0",
'avg_pmu': f"{statistics.mean(pmu_vals):.2f}" if pmu_vals else "0.00",
# Numeric values for accurate sorting
'_sort_count': count_val,
'_sort_max_usec': max(usecs),
'_sort_avg_usec': avg_usec_val,
'_sort_tot_usec': tot_usec_val,
'_sort_max_cycles': max(cycles),
'_sort_avg_cycles': statistics.mean(cycles),
'_sort_max_pmu': max(pmu_vals) if pmu_vals else 0,
'_sort_avg_pmu': statistics.mean(pmu_vals) if pmu_vals else 0
})
# Sorting logic
actual_sort_key = COL_MAP[sort_col][2]
is_numeric = actual_sort_key.startswith("_") or actual_sort_key == "count"
sorted_groups = sorted(group_stats, key=lambda x: x[actual_sort_key], reverse=is_numeric)[:top_n]
# Define initial column order
active_cols = ["op", "dims", "dtypes"]
if pmu_name:
active_cols += ["max-pmu", "avg-pmu"]
active_cols += ["tot-usec", "avg-usec", "avg-cycles", "max-usec", "max-cycles", "count"]
final_headers, final_keys, final_widths = [], [], []
for col_name in active_cols:
data_key, header_text, _ = COL_MAP[col_name]
if "pmu" in col_name and pmu_name:
header_text = header_text.replace("PMU", pmu_name)
natural_width = max([len(str(row[data_key])) for row in sorted_groups] + [len(header_text)])
target_width = width_overrides.get(col_name, natural_width)
if target_width == 0:
continue
final_headers.append(header_text)
final_keys.append(data_key)
final_widths.append(target_width)
# Print Report
logger.info(f"\n# Profile Report (Top {top_n} Ops sorted by {sort_col})\n")
header_line = "| " + " | ".join(f"{h:<{final_widths[i]}}" for i, h in enumerate(final_headers)) + " |"
sep_line = "| " + " | ".join("-" * final_widths[i] for i in range(len(final_headers))) + " |"
logger.info(header_line)
logger.info(sep_line)
for group in sorted_groups:
row_vals = []
for i, key in enumerate(final_keys):
val = str(group[key])
if len(val) > final_widths[i]:
val = val[:final_widths[i] - 3] + "..."
row_vals.append(f"{val:<{final_widths[i]}}")
logger.info("| " + " | ".join(row_vals) + " |")
def has_perf_print(file_path):
if file_path == "-":
return True
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
return any("common_perf_print" in line for line in f)
def pct_delta(a, b):
return (b - a) * 100.0 / a if a else None
def fmt_pct(p):
return f"{p:+.2f}%" if p is not None else "n/a"
def print_table(title, cols, rows, width_overrides):
# cols: list of (cli name, header); rows: list of dicts keyed by cli name
logger.info(f"\n# {title}\n")
if not rows:
logger.info("None.")
return
headers, keys, widths = [], [], []
for key, header in cols:
natural_width = max([len(str(row[key])) for row in rows] + [len(header)])
target_width = width_overrides.get(key, natural_width)
if target_width == 0:
continue
headers.append(header)
keys.append(key)
widths.append(target_width)
logger.info("| " + " | ".join(f"{h:<{widths[i]}}" for i, h in enumerate(headers)) + " |")
logger.info("| " + " | ".join("-" * widths[i] for i in range(len(headers))) + " |")
for row in rows:
row_vals = []
for i, key in enumerate(keys):
val = str(row[key])
if len(val) > widths[i]:
val = val[:widths[i] - 3] + "..."
row_vals.append(f"{val:<{widths[i]}}")
logger.info("| " + " | ".join(row_vals) + " |")
def generate_compare_report(ops_a, ops_b, file_a, file_b, top_n, width_overrides, min_delta):
def summarize(ops):
batches = [o for o in ops if o['name'] == "OPBATCH"]
others = [o for o in ops if o['name'] != "OPBATCH"]
mhz = [o['mhz'] for o in others if o['mhz'] is not None]
return {
'n_ops': len(others),
'tot_usec': sum(o['usec'] for o in others),
'n_batch': len(batches),
'batch_usec': sum(o['usec'] for o in batches),
'avg_mhz': statistics.mean(mhz) if mhz else 0.0,
}
sa, sb = summarize(ops_a), summarize(ops_b)
logger.info("\n# Compare Summary (OPBATCH excluded from op totals)\n")
logger.info(f" A (baseline) : {file_a}")
logger.info(f" B (new) : {file_b}")
logger.info(" Deltas are B - A: negative means B is faster\n")
logger.info(f" Ops : A {sa['n_ops']} B {sb['n_ops']}")
logger.info(f" Tot op usec : A {sa['tot_usec']} B {sb['tot_usec']} delta {sb['tot_usec'] - sa['tot_usec']:+d} ({fmt_pct(pct_delta(sa['tot_usec'], sb['tot_usec']))})")
logger.info(f" OPBATCH : A {sa['n_batch']} / {sa['batch_usec']} usec B {sb['n_batch']} / {sb['batch_usec']} usec")
logger.info(f" Avg op mhz : A {sa['avg_mhz']:.1f} B {sb['avg_mhz']:.1f}")
if sa['n_batch'] != sb['n_batch']:
logger.warning(f" WARNING: OPBATCH counts differ (A {sa['n_batch']}, B {sb['n_batch']}), one log may be incomplete")
for label, path in (("A", file_a), ("B", file_b)):
if not has_perf_print(path):
logger.warning(f" WARNING: log {label} has no common_perf_print line, run may be incomplete")
# Per-op-name rollup
by_name = defaultdict(lambda: [0, 0, 0, 0])
for idx, ops in ((0, ops_a), (1, ops_b)):
for op in ops:
if op['name'] == "OPBATCH":
continue
by_name[op['name']][idx] += 1
by_name[op['name']][idx + 2] += op['usec']
rows = []
for name, (cnt_a, cnt_b, tot_a, tot_b) in by_name.items():
p = pct_delta(tot_a, tot_b)
if min_delta and p is not None and abs(p) < min_delta:
continue
rows.append({'op': name, 'count-a': cnt_a, 'count-b': cnt_b, 'tot-usec-a': tot_a, 'tot-usec-b': tot_b,
'delta-usec': f"{tot_b - tot_a:+d}", 'delta-pct': fmt_pct(p), '_sort': abs(tot_b - tot_a)})
rows.sort(key=lambda r: r['_sort'], reverse=True)
print_table("Per-Op Rollup (delta = B - A, sorted by abs delta usec)", [
("op", "Op"), ("count-a", "Count A"), ("count-b", "Count B"), ("tot-usec-a", "Tot usec A"),
("tot-usec-b", "Tot usec B"), ("delta-usec", "Delta usec"), ("delta-pct", "Delta %")], rows, width_overrides)
# Per-group compare keyed on (op, dims, dtypes)
def group(ops):
grouped = defaultdict(list)
for op in ops:
if op['name'] != "OPBATCH":
grouped[(op['name'], op['dims'], op['types'])].append(op['usec'])
return grouped
ga, gb = group(ops_a), group(ops_b)
rows = []
for key in ga.keys() & gb.keys():
ua, ub = ga[key], gb[key]
tot_a, tot_b = sum(ua), sum(ub)
avg_a, avg_b = tot_a / len(ua), tot_b / len(ub)
p = pct_delta(avg_a, avg_b)
if min_delta and p is not None and abs(p) < min_delta:
continue
rows.append({'op': key[0], 'dims': key[1], 'dtypes': key[2], 'count-a': len(ua), 'count-b': len(ub),
'avg-usec-a': f"{avg_a:.2f}", 'avg-usec-b': f"{avg_b:.2f}", 'delta-pct': fmt_pct(p),
'tot-usec-a': tot_a, 'tot-usec-b': tot_b, 'delta-usec': f"{tot_b - tot_a:+d}", '_sort': abs(tot_b - tot_a)})
rows.sort(key=lambda r: r['_sort'], reverse=True)
print_table(f"Per-Group Compare (delta = B - A, top {top_n} sorted by abs delta usec)", [
("op", "Op"), ("dims", "Dims"), ("dtypes", "DTypes"), ("count-a", "Count A"), ("count-b", "Count B"),
("avg-usec-a", "Avg usec A"), ("avg-usec-b", "Avg usec B"), ("delta-pct", "Delta %"),
("tot-usec-a", "Tot usec A"), ("tot-usec-b", "Tot usec B"), ("delta-usec", "Delta usec")], rows[:top_n], width_overrides)
# Groups present in only one log
for label, g, other in (("A", ga, gb), ("B", gb, ga)):
rows = [{'op': k[0], 'dims': k[1], 'dtypes': k[2], 'count': len(v), 'tot-usec': sum(v)} for k, v in g.items() if k not in other]
rows.sort(key=lambda r: r['tot-usec'], reverse=True)
print_table(f"Groups only in {label} (Top {top_n} sorted by tot usec)", [
("op", "Op"), ("dims", "Dims"), ("dtypes", "DTypes"), ("count", "Count"), ("tot-usec", "Tot usec")], rows[:top_n], width_overrides)
def filter_ops(ops, args):
if args.filter:
try:
filter_re = re.compile(args.filter)
except re.error as e:
logger.error(f"Invalid regex filter: {e}")
sys.exit(1)
ops = [op for op in ops if filter_re.search(op['op_text'])]
if args.head is not None or args.tail is not None:
ops_by_dev = defaultdict(list)
for op in ops:
ops_by_dev[op['device']].append(op)
filtered_ops = []
for dev in sorted(ops_by_dev.keys()):
dev_ops = ops_by_dev[dev]
if args.head is not None:
dev_ops = dev_ops[:args.head]
elif args.tail is not None:
dev_ops = dev_ops[-args.tail:]
filtered_ops.extend(dev_ops)
ops = filtered_ops
return ops
def main():
parser = argparse.ArgumentParser(description="Post-process Op profile info.")
parser.add_argument("logfile")
parser.add_argument("-n", "--top", type=int, default=100)
parser.add_argument("--sort", type=str, default="tot-usec", choices=list(COL_MAP.keys()))
parser.add_argument("--pmu-index", type=int)
parser.add_argument("--pmu-name", type=str)
parser.add_argument("--width", action='append', default=['dims:40'], help="Override column width, e.g. --width dims:50")
parser.add_argument("--timeline", type=str, nargs='?', const='summary', choices=["summary", "bubbles"],
help="Output ASCII art event summary or thread idle bubble analysis (default: summary)")
parser.add_argument("--filter", type=str, help="Regex filter matching against the original profile-op line")
parser.add_argument("--device", type=str, help="Device to filter by (e.g. HTP0, HTP0:0) or 'split' to generate separate reports per device")
parser.add_argument("--compare", type=str, metavar="LOGFILE", help="Compare against another log (logfile is baseline A, this is B)")
parser.add_argument("--min-delta", type=float, default=0, metavar="PCT", help="In compare mode hide rows with abs(Delta %%) below PCT")
group = parser.add_mutually_exclusive_group()
group.add_argument("--head", type=int, help="Limit to first N ops")
group.add_argument("--tail", type=int, help="Limit to last N ops")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
if "pmu" in args.sort and args.pmu_index is None:
logger.error(f"Cannot sort by '{args.sort}' without --pmu-index.")
sys.exit(1)
if args.compare and (args.timeline or args.device == "split"):
logger.error("--compare cannot be used with --timeline or --device split.")
sys.exit(1)
overrides = {}
if args.width:
for w in args.width:
try:
name, val = w.split(':')
overrides[name.lower()] = int(val)
except ValueError:
logger.warning(f"Invalid width format '{w}'")
final_pmu_name = (args.pmu_name or f"#{args.pmu_index}") if args.pmu_index is not None else None
op_filter_re = None
if args.filter:
try:
op_filter_re = re.compile(args.filter)
except re.error as e:
logger.error(f"Invalid regex filter: {e}")
sys.exit(1)
limit = args.head if args.head is not None else None
device_filter = args.device if (args.device and args.device != "split") else None
ops = parse_log(args.logfile, pmu_index=args.pmu_index, limit=limit, device_filter=device_filter, op_filter_re=op_filter_re)
if args.device and args.device != "split":
ops = [op for op in ops if device_matches(op['device'], args.device)]
if args.compare:
ops_b = parse_log(args.compare, pmu_index=args.pmu_index, limit=limit, device_filter=device_filter, op_filter_re=op_filter_re)
if device_filter:
ops_b = [op for op in ops_b if device_matches(op['device'], device_filter)]
generate_compare_report(filter_ops(ops, args), filter_ops(ops_b, args), args.logfile, args.compare, args.top, overrides, args.min_delta)
return
if args.device == "split":
unique_devices = sorted(list(set(op['device'] for op in ops)))
for dev in unique_devices:
dev_ops = [op for op in ops if device_matches(op['device'], dev)]
if args.filter:
try:
filter_re = re.compile(args.filter)
except re.error as e:
logger.error(f"Invalid regex filter: {e}")
sys.exit(1)
dev_ops = [op for op in dev_ops if filter_re.search(op['op_text'])]
if args.head is not None:
dev_ops = dev_ops[:args.head]
elif args.tail is not None:
dev_ops = dev_ops[-args.tail:]
logger.info("\n=========================================")
logger.info(f" Device: {dev}")
logger.info("=========================================")
if args.timeline:
for op in dev_ops:
if args.timeline == "summary":
print_ascii_summary(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events'])
elif args.timeline == "bubbles":
print_bubbles_timeline(op)
else:
generate_report(dev_ops, args.top, overrides, args.sort, pmu_name=final_pmu_name)
else:
ops = filter_ops(ops, args)
if args.timeline:
for op in ops:
if args.timeline == "summary":
print_ascii_summary(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events'])
elif args.timeline == "bubbles":
print_bubbles_timeline(op)
else:
generate_report(ops, args.top, overrides, args.sort, pmu_name=final_pmu_name)
if __name__ == "__main__":
main()
+819
View File
@@ -0,0 +1,819 @@
#!/usr/bin/env python3
import sys
import os
import re
import argparse
import statistics
import logging
import bisect
from typing import Any, Dict, List, Optional
from collections import defaultdict
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
logger = logging.getLogger("ggml-hexagon-trace")
op_pattern = re.compile(
r"profile-op\s+(?P<op_name>[A-Z_0-9+]+):\s+.*?\s+:\s+(?P<dims>[\d:x\s\->!]+)\s+:\s+(?P<types>[a-z\d_\s\->x]+)\s+:\s+(?P<strides>[\d:x\s\->!]+?)\s+:\s+(?:(?P<params>.*?)\s+:\s+)?(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?"
)
trace_pattern = re.compile(
r"trace-evt\s+(?P<event>[A-Z_0-9\-]+):\s+thread\s+(?P<thread>\d+)\s+info\s+(?P<info>\d+)\s+(?P<state>start|stop)\s+(?P<cycles>\d+)"
)
device_pattern = re.compile(r"\b(HTP\d+(?::\d+)?)\s+(?:profile-op|trace-evt)\b")
def extract_device(line):
m = device_pattern.search(line)
if m:
return m.group(1)
return "HTP0"
def device_matches(record_device, target_device):
targets = [t.strip() for t in target_device.split(',')]
for target in targets:
if record_device == target:
return True
if record_device.startswith(target + ":"):
return True
return False
def get_split_output_path(base_path, device_name):
safe_device = device_name.replace(':', '_')
root, ext = os.path.splitext(base_path)
return f"{root}-{safe_device}{ext}"
def normalize_event_name(evt_type, info=0):
if evt_type == "HVX_COMP":
return "V-COMP"
if evt_type == "HMX_COMP":
return "M-COMP"
name = evt_type
if name.startswith("HVX_") or name.startswith("HMX_"):
name = name[4:]
return name.replace("_", "-")
class CycleUnwrapper:
def __init__(self, initial_val=None):
if initial_val is not None:
self.last_raw = initial_val & 0xFFFFFFFF
self.high_part = initial_val & 0xFFFFFFFF00000000
else:
self.last_raw = None
self.high_part = 0
def unwrap(self, raw):
if self.last_raw is None:
self.last_raw = raw
return raw
diff = raw - self.last_raw
if diff < -0x80000000:
self.high_part += 0x100000000
elif diff > 0x80000000:
self.high_part -= 0x100000000
self.last_raw = raw
return raw + self.high_part
class DeviceTimeMapper:
def __init__(self, dev, ops):
self.dev = dev
self.batches = []
for op in ops:
if op.get('device') == dev and op.get('name') == 'OPBATCH' and op.get('unwrapped_cycles_start') is not None:
cycles = op.get('cycles', 0)
usec = op.get('usec', 0)
start_cyc = op['unwrapped_cycles_start']
freq = (cycles / usec) if usec > 0 and cycles > 0 else 1000.0
if freq <= 0:
freq = 1000.0
self.batches.append({
'start_cycles': start_cyc,
'cycles': cycles,
'end_cycles': start_cyc + cycles,
'usec': usec,
'dur_ns': usec * 1000,
'freq_mhz': freq,
})
self.batches.sort(key=lambda b: b['start_cycles'])
for i, b in enumerate(self.batches):
if i == 0:
b['start_time_ns'] = 0
else:
prev = self.batches[i - 1]
idle_cyc = max(0, b['start_cycles'] - prev['end_cycles'])
idle_ns = int(round((idle_cyc / prev['freq_mhz']) * 1000))
b['start_time_ns'] = prev['start_time_ns'] + prev['dur_ns'] + idle_ns
self.batch_starts = [b['start_cycles'] for b in self.batches]
valid_starts = [op['unwrapped_cycles_start'] for op in ops if op.get('device') == dev and op.get('unwrapped_cycles_start') is not None]
self.min_cyc = min(valid_starts) if valid_starts else 0
if self.batches:
self.default_freq = self.batches[0]['freq_mhz']
else:
freqs = [op['cycles'] / op['usec'] for op in ops if op.get('device') == dev and op.get('usec', 0) > 0 and op.get('cycles', 0) > 0]
self.default_freq = statistics.mean(freqs) if freqs else 1000.0
def get_batch(self, cyc):
if not self.batches:
return None
idx = bisect.bisect_right(self.batch_starts, cyc) - 1
if idx >= 0:
return self.batches[idx]
return self.batches[0]
def get_freq(self, cyc=None):
if cyc is not None:
b = self.get_batch(cyc)
if b is not None:
return b['freq_mhz']
return self.default_freq
def cycle_to_ns(self, cyc):
if cyc is None:
return 0
b = self.get_batch(cyc)
if b is not None:
return b['start_time_ns'] + int(round(((cyc - b['start_cycles']) / b['freq_mhz']) * 1000))
return int(round(((cyc - self.min_cyc) / self.default_freq) * 1000))
def dur_cycles_to_ns(self, cyc_start, cyc_dur):
if cyc_dur is None:
return 0
freq = self.get_freq(cyc_start)
return int(round((cyc_dur / freq) * 1000))
def parse_log(file_path, limit=None, device_filter=None, op_filter_re=None):
try:
if file_path != "-":
f = open(file_path, 'r', encoding='utf-8', errors='ignore')
else:
f = os.fdopen(0, 'r', encoding='utf-8', errors='ignore')
except FileNotFoundError:
logger.error(f"file '{file_path}' not found.")
sys.exit(1)
all_ops: List[Dict[str, Any]] = []
all_traces: List[Dict[str, Any]] = []
current_op: Optional[Dict[str, Any]] = None
ops_count_per_device = {}
if device_filter is not None:
for target in device_filter.split(','):
ops_count_per_device[target.strip()] = 0
limit_reached = False
unwrappers = {}
last_batch_start = {}
trace_unwrappers = {}
line_idx = 0
for line in f:
line_idx += 1
if "profile-op" not in line and "trace-evt" not in line:
continue
device = extract_device(line)
idx = line.find("profile-op")
if idx != -1 and "|" in line[idx:]:
parts = [p.strip() for p in line[idx:].split("|")]
prefix = parts[0]
prefix_match = re.search(r"profile-op\s+(?P<op_name>[A-Z_0-9+]+)", prefix)
if not prefix_match:
continue
names = parts[1]
if len(parts) == 7:
dims, types, strides, params, timings = parts[2], parts[3], parts[4], parts[5], parts[6]
elif len(parts) == 6:
dims, types, strides, params, timings = parts[2], parts[3], parts[4], "", parts[5]
else:
continue
timing_match = re.search(
r"(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?",
timings
)
if not timing_match:
continue
op_match = timing_match
op_name = prefix_match.group("op_name")
else:
op_match = op_pattern.search(line)
if op_match:
op_name = op_match.group('op_name')
names = ""
dims = op_match.group('dims').strip() if op_match.group('dims') else ''
types = op_match.group('types').strip() if op_match.group('types') else ''
strides = op_match.group('strides').strip() if op_match.group('strides') else ''
params = op_match.group('params').strip() if ('params' in op_match.groupdict() and op_match.group('params')) else ''
else:
op_match = None
if op_match:
cycles_start_raw = op_match.group('start')
unwrapped_cycles_start = None
if op_name == "OPBATCH":
if cycles_start_raw:
unwrapped_cycles_start = int(cycles_start_raw)
unwrappers[device] = CycleUnwrapper(unwrapped_cycles_start)
last_batch_start[device] = unwrapped_cycles_start
for k in list(trace_unwrappers.keys()):
if k[0] == device:
del trace_unwrappers[k]
else:
if cycles_start_raw:
device_unwrapper = unwrappers.get(device)
if device_unwrapper is not None:
unwrapped_cycles_start = device_unwrapper.unwrap(int(cycles_start_raw))
op_text = re.sub(r"^profile-op\s+", "", line[idx:]).strip() if idx != -1 else line.strip()
evt_str = None
if types.startswith("evt-cnt "):
evt_str = types[8:].strip()
current_op = {
'name': op_name,
'names': names,
'dims': dims,
'types': types,
'strides': strides,
'params': params,
'evt': evt_str,
'op_text': op_text,
'usec': int(op_match.group('usec')),
'cycles': int(op_match.group('cycles')),
'cycles_start': int(cycles_start_raw) if cycles_start_raw else None,
'unwrapped_cycles_start': unwrapped_cycles_start,
'trace_events': [],
'line_num': line_idx,
'device': device
}
all_ops.append(current_op)
# Check if matching early exit criteria
matched = False
matched_target = None
if device_filter is not None:
targets = [t.strip() for t in device_filter.split(',')]
for target in targets:
if device == target or device.startswith(target + ":"):
matched = True
matched_target = target
break
else:
matched = True
matched_target = device
if op_filter_re is not None and not op_filter_re.search(op_text):
matched = False
if matched:
if matched_target not in ops_count_per_device:
ops_count_per_device[matched_target] = 0
ops_count_per_device[matched_target] += 1
if limit is not None and len(ops_count_per_device) > 0 and all(count >= limit for count in ops_count_per_device.values()):
limit_reached = True
if limit_reached and op_name == "OPBATCH":
break
continue
trace_match = trace_pattern.search(line)
if trace_match:
thread = int(trace_match.group('thread'))
raw_cyc = int(trace_match.group('cycles'))
unwrapped_cyc = None
th_key = (device, thread)
if th_key not in trace_unwrappers:
batch_start = last_batch_start.get(device)
trace_unwrappers[th_key] = CycleUnwrapper(batch_start)
unwrapped_cyc = trace_unwrappers[th_key].unwrap(raw_cyc)
all_traces.append({
'thread': thread,
'event': trace_match.group('event'),
'info': int(trace_match.group('info')),
'cycles': raw_cyc,
'unwrapped_cycles': unwrapped_cyc,
'state': trace_match.group('state'),
'line_num': line_idx,
'device': device
})
f.close()
return all_ops, all_traces
# --- Simple protobuf encoder ---
def write_varint(val):
if val < 0:
val = (1 << 64) + val
res = bytearray()
while True:
towrite = val & 0x7f
val >>= 7
if val > 0:
res.append(towrite | 0x80)
else:
res.append(towrite)
break
return bytes(res)
def pb_field(num, wire, data):
return write_varint((num << 3) | wire) + data
def pb_varint(num, val):
return pb_field(num, 0, write_varint(val))
def pb_length_delimited(num, data):
return pb_field(num, 2, write_varint(len(data)) + data)
def pb_string(num, text):
return pb_length_delimited(num, text.encode('utf-8'))
# Message Encoders
def make_process_descriptor(pid, name):
return pb_varint(1, pid) + pb_string(6, name)
def make_thread_descriptor(pid, tid, name, sort_index=None):
payload = pb_varint(1, pid) + pb_varint(2, tid) + pb_string(5, name)
if sort_index is not None:
payload += pb_varint(3, sort_index)
return payload
def make_track_descriptor(uuid, name=None, parent_uuid=None, thread=None, process=None, sibling_merge_behavior=None, child_ordering=None, sibling_order_rank=None):
payload = pb_varint(1, uuid)
if name is not None:
payload += pb_string(2, name)
if parent_uuid is not None:
payload += pb_varint(5, parent_uuid)
if process is not None:
payload += pb_length_delimited(3, process)
if thread is not None:
payload += pb_length_delimited(4, thread)
if sibling_merge_behavior is not None:
payload += pb_varint(15, sibling_merge_behavior)
if child_ordering is not None:
payload += pb_varint(11, child_ordering)
if sibling_order_rank is not None:
payload += pb_varint(12, sibling_order_rank)
return payload
def make_debug_annotation(name, string_val=None, int_val=None):
payload = pb_string(10, name)
if string_val is not None:
payload += pb_string(6, string_val)
elif int_val is not None:
payload += pb_varint(4, int_val)
return payload
def make_track_event(event_type, track_uuid, name=None, category=None, debug_annotations=None):
payload = pb_varint(9, event_type)
payload += pb_varint(11, track_uuid)
if name is not None:
payload += pb_string(23, name)
if category is not None:
payload += pb_string(22, category)
if debug_annotations is not None:
for da in debug_annotations:
payload += pb_length_delimited(4, da)
return payload
def make_trace_packet(timestamp, track_event=None, track_descriptor=None, seq_id=1):
payload = pb_varint(8, timestamp)
payload += pb_varint(10, seq_id)
if track_event is not None:
payload += pb_length_delimited(11, track_event)
if track_descriptor is not None:
payload += pb_length_delimited(60, track_descriptor)
return payload
def write_trace_packet_to_file(f, packet_bytes):
# Write as field 1 of top-level Trace message
f.write(pb_length_delimited(1, packet_bytes))
# --- End Protobuf Encoder ---
def generate_perfetto_trace(filtered_ops, trace_events, output_path):
if not filtered_ops:
logger.warning("No operators found after filtering.")
return
# Assign start and end cycles to each operator
for op in filtered_ops:
op['start_cycles'] = op['unwrapped_cycles_start']
op['end_cycles'] = op['start_cycles'] + op['cycles'] if op['start_cycles'] is not None else None
# Get list of unique devices present in the operations
unique_devices = sorted(list(set(op['device'] for op in filtered_ops)))
device_to_idx = {dev: idx for idx, dev in enumerate(unique_devices)}
time_mappers = {dev: DeviceTimeMapper(dev, filtered_ops) for dev in unique_devices}
# Process events
completed_events = []
if trace_events:
trace_events = sorted(trace_events, key=lambda e: e['unwrapped_cycles'])
one_usec_cycles = {}
for dev in unique_devices:
one_usec_cycles[dev] = max(time_mappers[dev].get_freq(), 1.0)
active_starts = {}
for e in trace_events:
t = e['thread']
evt = e['event']
info = e['info']
state = e['state']
cyc = e['unwrapped_cycles']
dev = e['device']
key = (dev, t, evt, info)
if state == 'start':
# Handle missing stop (start followed by another start)
if key in active_starts:
prev_e = active_starts[key]
completed_events.append({
'thread': t,
'event': evt,
'info': info,
'start_cyc': prev_e['unwrapped_cycles'],
'end_cyc': prev_e['unwrapped_cycles'] + one_usec_cycles.get(dev, 1000.0),
'line_num': prev_e.get('line_num'),
'missing_stop': True,
'device': dev
})
active_starts[key] = e
elif state == 'stop':
if key in active_starts:
prev_e = active_starts[key]
del active_starts[key]
completed_events.append({
'thread': t,
'event': evt,
'info': info,
'start_cyc': prev_e['unwrapped_cycles'],
'end_cyc': cyc,
'line_num': prev_e.get('line_num'),
'device': dev
})
else:
# Handle missing start (stop without start)
completed_events.append({
'thread': t,
'event': evt,
'info': info,
'start_cyc': cyc - one_usec_cycles.get(dev, 1000.0),
'end_cyc': cyc,
'line_num': e.get('line_num'),
'missing_start': True,
'device': dev
})
# Clear remaining unmatched starts
for key, prev_e in active_starts.items():
dev, t, evt, info = key
completed_events.append({
'thread': t,
'event': evt,
'info': info,
'start_cyc': prev_e['unwrapped_cycles'],
'end_cyc': prev_e['unwrapped_cycles'] + one_usec_cycles.get(dev, 1000.0),
'line_num': prev_e.get('line_num'),
'missing_stop': True,
'device': dev
})
completed_events.sort(key=lambda e: e['start_cyc'])
# Convert event times to nanoseconds using per-device / per-batch time mapper
for e in completed_events:
dev = e['device']
tm = time_mappers[dev]
e['ts_ns'] = tm.cycle_to_ns(e['start_cyc'])
dur_ns = tm.dur_cycles_to_ns(e['start_cyc'], e['end_cyc'] - e['start_cyc'])
e['dur_ns'] = max(dur_ns, 1)
# Allocate slots (sub-tracks) to prevent overlaps on same virtual track
active_slots = defaultdict(list)
for e in completed_events:
t = e['thread']
evt = e['event']
ts = e['ts_ns']
dur = e['dur_ns']
dev = e['device']
norm_evt = normalize_event_name(evt, e['info'])
if norm_evt == "DMA":
track_key = (dev, t, "DMA")
elif t == 10:
track_key = (dev, t, "HMX")
else:
track_key = (dev, t, "HVX")
slots = active_slots[track_key]
allocated_slot = -1
for idx, slot_end_ns in enumerate(slots):
if ts >= slot_end_ns:
slots[idx] = ts + dur
allocated_slot = idx
break
if allocated_slot == -1:
slots.append(ts + dur)
allocated_slot = len(slots) - 1
e['slot'] = allocated_slot
# Generate Track IDs and track definitions
used_tracks = {}
for e in completed_events:
t = e['thread']
evt = e['event']
slot = e['slot']
dev = e['device']
norm_evt = normalize_event_name(evt, e['info'])
if norm_evt == "DMA":
track_evt = "DMA"
evt_id = 1
elif t == 10:
track_evt = "HMX"
evt_id = 3
else:
track_evt = "HVX"
evt_id = 2
t_sort = 1 if t == 10 else t + 2
dev_idx = device_to_idx[dev]
# Unique UUID for each sub-track
if t == 10:
uuid = dev_idx * 10000000 + 20 # HMX thread track UUID
else:
uuid = int(dev_idx * 10000000 + t_sort * 1000000 + evt_id * 1000 + slot)
e['uuid'] = uuid
used_tracks[uuid] = (dev, t, track_evt, slot)
with open(output_path, "wb") as f:
for dev in unique_devices:
dev_idx = device_to_idx[dev]
pid = dev_idx + 1
proc_uuid = dev_idx * 10000000 + 1
# Define Process with EXPLICIT child sorting
proc_name = dev
proc_desc = make_process_descriptor(pid, proc_name)
proc_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(proc_uuid, process=proc_desc, child_ordering=3))
write_trace_packet_to_file(f, proc_packet)
# Define Operators Track as a thread track
op_track_uuid = dev_idx * 10000000 + 2
op_tid = pid * 100 + 8
op_thread_desc = make_thread_descriptor(pid, op_tid, "Ops", sort_index=1)
op_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(op_track_uuid, parent_uuid=proc_uuid, thread=op_thread_desc))
write_trace_packet_to_file(f, op_packet)
# Define HMX Thread Track at rank 2
hmx_track_uuid = dev_idx * 10000000 + 20
hmx_tid = pid * 100 + 9
hmx_thread_desc = make_thread_descriptor(pid, hmx_tid, "HMX", sort_index=2)
hmx_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(hmx_track_uuid, parent_uuid=proc_uuid, thread=hmx_thread_desc))
write_trace_packet_to_file(f, hmx_packet)
# Define Thread Tracks (T0, T1, ..., T9) for this device
dev_used_tracks = {uuid: val for uuid, val in used_tracks.items() if val[0] == dev}
unique_threads = sorted(list(set(t for (_, t, _, _) in dev_used_tracks.values() if t != 10)))
for t in unique_threads:
thread_uuid = dev_idx * 10000000 + 10 + t
thread_name = f"T{t}"
sort_index = 3 + t
tid = pid * 100 + 10 + t
thread_desc = make_thread_descriptor(pid, tid, thread_name, sort_index=sort_index)
thread_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(
thread_uuid,
parent_uuid=proc_uuid,
thread=thread_desc,
sibling_order_rank=sort_index,
child_ordering=3 # Explicit child sorting for sub-tracks
))
write_trace_packet_to_file(f, thread_packet)
# Define Track descriptors for sub-tracks parented to thread tracks
for uuid in sorted(used_tracks.keys()):
dev, t, evt, slot = used_tracks[uuid]
dev_idx = device_to_idx[dev]
if t == 10:
continue
name = f"T{t} {evt}"
rank = 0 if evt == "HVX" else 1
parent_thread_uuid = dev_idx * 10000000 + 10 + t
# Sibling merge behavior: 1 (SIBLING_MERGE_BEHAVIOR_BY_TRACK_NAME)
track_desc = make_track_descriptor(
uuid=uuid,
name=name,
parent_uuid=parent_thread_uuid,
sibling_merge_behavior=1,
sibling_order_rank=rank
)
track_packet = make_trace_packet(0, track_descriptor=track_desc)
write_trace_packet_to_file(f, track_packet)
# Emit Operators
last_op_end_ns = defaultdict(int)
for op in filtered_ops:
dev = op['device']
dev_idx = device_to_idx[dev]
tm = time_mappers[dev]
op_start_ns = tm.cycle_to_ns(op['start_cycles'])
op_dur_ns = tm.dur_cycles_to_ns(op['start_cycles'], op['cycles'])
if op['name'] != "OPBATCH":
if op_start_ns < last_op_end_ns[dev]:
op_start_ns = last_op_end_ns[dev]
clamped_dur = max(op_dur_ns, 100) # Clamp to 100ns (0.1us)
last_op_end_ns[dev] = op_start_ns + clamped_dur
else:
clamped_dur = max(op_dur_ns, 100)
# Debug annotations for Ops
debug_annots = []
if 'line_num' in op:
debug_annots.append(make_debug_annotation("line", int_val=op['line_num']))
if 'names' in op and op['names'] and op['names'] != '----':
debug_annots.append(make_debug_annotation("names", string_val=op['names']))
if 'strides' in op and op['strides'] and op['strides'] != '----':
debug_annots.append(make_debug_annotation("strides", string_val=op['strides']))
if 'params' in op and op['params'] and op['params'] != '----':
debug_annots.append(make_debug_annotation("params", string_val=op['params']))
if 'evt' in op and op['evt']:
debug_annots.append(make_debug_annotation("evt", string_val=op['evt']))
op_track_uuid = dev_idx * 10000000 + 2
# Slice Begin
evt_begin = make_track_event(1, op_track_uuid, name=f"{op['name']} ({op['dims']})", category="operator", debug_annotations=debug_annots)
packet_begin = make_trace_packet(op_start_ns, track_event=evt_begin)
write_trace_packet_to_file(f, packet_begin)
# Slice End
evt_end = make_track_event(2, op_track_uuid)
packet_end = make_trace_packet(op_start_ns + clamped_dur, track_event=evt_end)
write_trace_packet_to_file(f, packet_end)
# Emit Thread Trace Events
for e in completed_events:
norm_name = normalize_event_name(e['event'], e['info'])
if norm_name == "DMA":
name = f"DMA {e['info']}"
elif norm_name == "FENCE":
name = f"FENCE {e['info']}" if e.get('info') is not None and e['info'] != 0 else "FENCE"
else:
name = norm_name
if e.get('missing_start') or e.get('missing_stop'):
name += "!"
debug_annots = []
if 'line_num' in e and e['line_num'] is not None:
debug_annots.append(make_debug_annotation("line", int_val=e['line_num']))
if norm_name == "FENCE" and e.get('info') is not None:
debug_annots.append(make_debug_annotation("seq", int_val=e['info']))
elif norm_name == "DMA" and e.get('info') is not None:
debug_annots.append(make_debug_annotation("channel", int_val=e['info']))
elif e.get('info') is not None and e['info'] != 0:
debug_annots.append(make_debug_annotation("info", int_val=e['info']))
if e.get('missing_start'):
debug_annots.append(make_debug_annotation("missing_start", string_val="true"))
if e.get('missing_stop'):
debug_annots.append(make_debug_annotation("missing_stop", string_val="true"))
# Slice Begin
evt_begin = make_track_event(1, e['uuid'], name=name, category="trace", debug_annotations=debug_annots if debug_annots else None)
packet_begin = make_trace_packet(e['ts_ns'], track_event=evt_begin)
write_trace_packet_to_file(f, packet_begin)
# Slice End
evt_end = make_track_event(2, e['uuid'])
packet_end = make_trace_packet(e['ts_ns'] + e['dur_ns'], track_event=evt_end)
write_trace_packet_to_file(f, packet_end)
logger.info(f"Successfully generated Perfetto trace at {output_path}")
def main():
parser = argparse.ArgumentParser(description="Convert Hexagon Op profile logs to native Perfetto Protobuf traces.")
parser.add_argument("logfile", help="Path to hex-log profile file")
parser.add_argument("-o", "--output", default="optrace.perfetto-trace", help="Output trace file path (default: optrace.perfetto-trace)")
parser.add_argument("--filter", type=str, help="Regex filter matching against the original profile-op line")
parser.add_argument("--device", type=str, help="Device to filter by (e.g. HTP0, HTP0:0) or 'split' to generate separate files per device")
group = parser.add_mutually_exclusive_group()
group.add_argument("--head", type=int, help="Limit to first N ops")
group.add_argument("--tail", type=int, help="Limit to last N ops")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
op_filter_re = None
if args.filter:
try:
op_filter_re = re.compile(args.filter)
except re.error as e:
logger.error(f"Invalid regex filter: {e}")
sys.exit(1)
limit = args.head if args.head is not None else None
device_filter = args.device if (args.device and args.device != "split") else None
ops, traces = parse_log(args.logfile, limit=limit, device_filter=device_filter, op_filter_re=op_filter_re)
if args.device and args.device != "split":
ops = [op for op in ops if device_matches(op['device'], args.device)]
traces = [t for t in traces if device_matches(t['device'], args.device)]
if args.filter:
try:
filter_re = re.compile(args.filter)
except re.error as e:
logger.error(f"Invalid regex filter: {e}")
sys.exit(1)
ops = [op for op in ops if filter_re.search(op['op_text'])]
if args.head is not None or args.tail is not None:
ops_by_dev = defaultdict(list)
for op in ops:
ops_by_dev[op['device']].append(op)
filtered_ops = []
for dev in sorted(ops_by_dev.keys()):
dev_ops = ops_by_dev[dev]
if args.head is not None:
dev_ops = dev_ops[:args.head]
elif args.tail is not None:
dev_ops = dev_ops[-args.tail:]
filtered_ops.extend(dev_ops)
ops = filtered_ops
if args.filter or args.head is not None or args.tail is not None:
# Group valid ranges by device
valid_ranges_by_dev = defaultdict(list)
for op in ops:
start_cyc = op['unwrapped_cycles_start']
end_cyc = start_cyc + op['cycles'] if start_cyc is not None else None
if start_cyc is not None and end_cyc is not None:
valid_ranges_by_dev[op['device']].append((start_cyc, end_cyc))
for dev in valid_ranges_by_dev:
valid_ranges_by_dev[dev].sort(key=lambda r: r[0])
range_starts_by_dev = {dev: [r[0] for r in ranges] for dev, ranges in valid_ranges_by_dev.items()}
filtered_traces = []
for e in traces:
cyc = e['unwrapped_cycles']
if cyc is None:
continue
dev = e['device']
range_starts = range_starts_by_dev.get(dev)
if not range_starts:
continue
idx = bisect.bisect_right(range_starts, cyc) - 1
if idx >= 0:
start, end = valid_ranges_by_dev[dev][idx]
if start <= cyc <= end:
filtered_traces.append(e)
traces = filtered_traces
if args.device == "split":
unique_devices = sorted(list(set(op['device'] for op in ops)))
for dev in unique_devices:
dev_ops = [op for op in ops if device_matches(op['device'], dev)]
dev_traces = [t for t in traces if device_matches(t['device'], dev)]
out_path = get_split_output_path(args.output, dev)
generate_perfetto_trace(dev_ops, dev_traces, out_path)
else:
generate_perfetto_trace(ops, traces, args.output)
if __name__ == "__main__":
main()
@@ -0,0 +1,22 @@
Appium-Python-Client==5.2.4
attrs==25.4.0
certifi==2025.10.5
exceptiongroup==1.3.0
h11==0.16.0
idna==3.11
iniconfig==2.1.0
outcome==1.3.0.post0
packaging==25.0
pluggy==1.6.0
PySocks==1.7.1
pytest==8.4.2
selenium==4.36.0
sniffio==1.3.1
sortedcontainers==2.4.0
tomli==2.3.0
trio==0.31.0
trio-websocket==0.12.2
typing_extensions==4.15.0
urllib3==2.5.0
websocket-client==1.9.0
wsproto==1.2.0
@@ -0,0 +1,663 @@
"""Run llama.cpp Hexagon tests in a single QDC job.
Bundles test scripts into one artifact and submits a single QDC job:
1. run_bench_tests_posix.py — llama-cli and llama-bench on CPU / GPU / NPU
(from scripts/snapdragon/qdc/)
Results are written to $GITHUB_STEP_SUMMARY when set (GitHub Actions).
Prerequisites:
pip install /path/to/qualcomm_device_cloud_sdk*.whl
Platform is inferred from --device:
android Appium + pytest (Android phones: SM8750 / SM8650 / SM8850)
linux BASH (Linux IoT: QCS9075M)
Required environment variables:
QDC_API_KEY API key from QDC UI -> Users -> Settings -> API Keys
Usage:
python run_qdc_jobs.py \\
--pkg-dir pkg-snapdragon/llama.cpp \\
--model-url https://.../Llama-3.2-1B-Instruct-Q4_0.gguf \\
--device SM8750
"""
from __future__ import annotations
import argparse
import enum
import logging
import os
import re
import shutil
import sys
import tempfile
import time
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
from qualcomm_device_cloud_sdk.api import qdc_api
from qualcomm_device_cloud_sdk.logging import configure_logging
from qualcomm_device_cloud_sdk.models import (
ArtifactType,
JobMode,
JobState,
JobSubmissionParameter,
JobType,
TestFramework,
)
# configure_logging only sets up the SDK logger; basicConfig is needed for
# our own log.info to reach stdout.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()],
)
configure_logging(level=logging.INFO, handlers=[logging.StreamHandler()])
# Silence per-poll GET/status spam from the SDK and its HTTP client.
logging.getLogger("qualcomm_device_cloud").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
log = logging.getLogger(__name__)
POLL_INTERVAL = 30
JOB_TIMEOUT = 3600
LOG_UPLOAD_TIMEOUT = 600
CAPACITY_TIMEOUT = 1800
CAPACITY_POLL = 60
MAX_CONCURRENT_JOBS = 5
DEFAULT_RETRIES = 0
RETRY_DELAY = 300
TERMINAL_STATES = {JobState.COMPLETED, JobState.CANCELED}
NON_TERMINAL_STATES = {JobState.DISPATCHED, JobState.RUNNING, JobState.SETUP, JobState.SUBMITTED}
class DeviceUnavailableError(Exception):
"""Raised when the QDC device resource is not available (retryable)."""
_SCRIPTS_DIR = Path(__file__).parent
_TESTS_DIR = _SCRIPTS_DIR / "tests"
# --- Shared test assets -------------------------------------------------------
_UTILS = _TESTS_DIR / "utils.py"
_CONFTEST = _TESTS_DIR / "conftest.py"
_PYTEST_LINE_RE = re.compile(
r"(?:[\w/]+\.py::)?(?:\w+::)?([\w\[\].-]+)\s+(PASSED|FAILED|ERROR|SKIPPED)"
)
_EXCLUDED_LOGS = {
"qdc_android_whole_host-000.log",
"qdc_kernel_host-000.log",
"qdc_LE_whole_host-000.log",
"qdc_LE_kernel_host-000.log",
"script.log",
}
_NON_TERMINAL_STATE_VALUES = {s.value for s in NON_TERMINAL_STATES}
# --- Android (Appium + pytest) assets ----------------------------------------
_RUN_BENCH = _TESTS_DIR / "run_bench_tests_posix.py"
_RUN_BACKEND_OPS = _TESTS_DIR / "run_backend_ops_posix.py"
_REQUIREMENTS = _SCRIPTS_DIR / "requirements.txt"
_RUN_PY = _SCRIPTS_DIR.parent / "run.py"
# --- Linux (BASH) assets ------------------------------------------------------
_RUN_LINUX_TEMPLATE = _TESTS_DIR / "linux" / "run_linux.sh"
_LINUX_ENTRY_SCRIPT = "/bin/bash /data/local/tmp/TestContent/run_linux.sh"
# =============================================================================
# Artifact builders (per platform)
# =============================================================================
@dataclass
class JobResult:
passed: bool
tests: dict[str, bool] = field(default_factory=dict)
raw_logs: dict[str, str] = field(default_factory=dict)
failure_details: dict[str, str] = field(default_factory=dict)
def _write_lf(path: Path, content: str) -> None:
"""Write text with LF line endings (required by /bin/bash on Linux)."""
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(content)
def _build_android_artifact(
pkg_dir: Path,
stage_dir: Path,
test_mode: str,
model_url: str | None,
) -> Path:
"""Android zip (Appium/pytest). Extracted by QDC under /qdc/appium/.
Zip structure:
llama_cpp_bundle/ installed package (adb pushed to /data/local/tmp/)
run.py Snapdragon runner
tests/
utils.py shared adb helpers
conftest.py Appium pytest fixtures
test_bench_posix.py bench + cli tests (for --test bench or all)
test_backend_ops_posix.py test-backend-ops on HTP0
requirements.txt
pytest.ini addopts = --junitxml=results.xml
"""
bundle_dir = stage_dir / "llama_cpp_bundle"
shutil.copytree(pkg_dir, bundle_dir)
dest = stage_dir / "run.py"
shutil.copy(_RUN_PY, dest)
dest.chmod(0o755)
tests_dir = stage_dir / "tests"
tests_dir.mkdir()
shutil.copy(_UTILS, tests_dir / "utils.py")
shutil.copy(_CONFTEST, tests_dir / "conftest.py")
if test_mode in ("bench", "all"):
assert model_url is not None
(tests_dir / "test_bench_posix.py").write_text(
_RUN_BENCH.read_text().replace("<<MODEL_URL>>", model_url)
)
if test_mode in ("backend-ops", "all"):
shutil.copy(_RUN_BACKEND_OPS, tests_dir / "test_backend_ops_posix.py")
shutil.copy(_REQUIREMENTS, stage_dir / "requirements.txt")
(stage_dir / "pytest.ini").write_text(
"[pytest]\naddopts = --junitxml=results.xml\n"
)
zip_base = str(stage_dir / "artifact")
shutil.make_archive(zip_base, "zip", stage_dir)
return Path(f"{zip_base}.zip")
def _build_linux_artifact(
pkg_dir: Path,
stage_dir: Path,
test_mode: str,
model_url: str | None,
) -> Path:
"""Linux IoT zip (BASH framework). Extracted by QDC to /data/local/tmp/TestContent/.
Zip structure:
run_linux.sh entry script (placeholder-substituted, LF line endings)
llama_cpp_bundle/ installed package
"""
bundle_dir = stage_dir / "llama_cpp_bundle"
shutil.copytree(pkg_dir, bundle_dir)
template = _RUN_LINUX_TEMPLATE.read_text(encoding="utf-8")
rendered = template.replace("{MODEL_URL}", model_url or "").replace(
"{TEST_MODE}", test_mode
)
script_path = stage_dir / "run_linux.sh"
_write_lf(script_path, rendered)
script_path.chmod(0o755)
zip_base = str(stage_dir / "artifact")
shutil.make_archive(zip_base, "zip", stage_dir)
return Path(f"{zip_base}.zip")
# =============================================================================
# Platform enum + strategy table
# =============================================================================
class Platform(enum.Enum):
ANDROID = "android"
LINUX = "linux"
@dataclass(frozen=True)
class PlatformSpec:
test_framework: TestFramework
entry_script: str | None
build_artifact: Callable[[Path, Path, str, str | None], Path]
job_name_fmt: str
PLATFORM_SPECS: dict[Platform, PlatformSpec] = {
Platform.ANDROID: PlatformSpec(
test_framework=TestFramework.APPIUM,
entry_script=None,
build_artifact=_build_android_artifact,
job_name_fmt="{base}",
),
Platform.LINUX: PlatformSpec(
test_framework=TestFramework.BASH,
entry_script=_LINUX_ENTRY_SCRIPT,
build_artifact=_build_linux_artifact,
job_name_fmt="{base} (Linux)",
),
}
DEVICE_PLATFORM: dict[str, Platform] = {
"SM8750": Platform.ANDROID,
"SM8650": Platform.ANDROID,
"SM8850": Platform.ANDROID,
"QCS9075M": Platform.LINUX,
}
# =============================================================================
# Shared QDC job plumbing
# =============================================================================
def wait_for_job(client, job_id: str, timeout: int) -> str:
elapsed = 0
last_state = None
consecutive_errors = 0
max_consecutive_errors = 5
while elapsed < timeout:
try:
raw = qdc_api.get_job_status(client, job_id)
consecutive_errors = 0
except Exception as e:
consecutive_errors += 1
log.warning(
"Transient error polling job %s (%d/%d): %s",
job_id,
consecutive_errors,
max_consecutive_errors,
e,
)
if consecutive_errors >= max_consecutive_errors:
raise
time.sleep(POLL_INTERVAL)
elapsed += POLL_INTERVAL
continue
try:
status = JobState(raw)
except ValueError:
status = raw
if status in TERMINAL_STATES:
return raw.lower()
if raw != last_state:
log.info("Job %s: %s", job_id, raw)
last_state = raw
time.sleep(POLL_INTERVAL)
elapsed += POLL_INTERVAL
# Abort to free the QDC concurrency slot instead of leaking it.
try:
qdc_api.abort_job(client, job_id)
log.warning("Aborted job %s after timeout to free concurrency slot", job_id)
except Exception as e:
log.warning("Failed to abort job %s: %s", job_id, e)
raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")
def wait_for_log_upload(client, job_id: str) -> None:
elapsed = 0
while elapsed <= LOG_UPLOAD_TIMEOUT:
try:
status = (qdc_api.get_job_log_upload_status(client, job_id) or "").lower()
except Exception as e:
log.warning("get_job_log_upload_status failed: %s — will retry", e)
status = ""
if status in {"completed", "failed"}:
return
log.info("Waiting for log upload (status=%s) ...", status)
time.sleep(POLL_INTERVAL)
elapsed += POLL_INTERVAL
log.warning("Timed out waiting for log upload after %ds", LOG_UPLOAD_TIMEOUT)
def wait_for_capacity(client, max_jobs: int = MAX_CONCURRENT_JOBS) -> None:
"""Block until the user's active (non-terminal) QDC job count is below max_jobs."""
elapsed = 0
while elapsed < CAPACITY_TIMEOUT:
jobs_page = qdc_api.get_jobs_list(client, page_number=0, page_size=50)
if jobs_page is None:
log.warning(
"Could not retrieve job list; proceeding without capacity check"
)
return
items = getattr(jobs_page, "data", []) or []
active = sum(
1 for j in items if getattr(j, "state", None) in _NON_TERMINAL_STATE_VALUES
)
if active < max_jobs:
log.info("Active QDC jobs: %d / %d — proceeding", active, max_jobs)
return
log.info(
"Active QDC jobs: %d / %d — waiting %ds ...",
active,
max_jobs,
CAPACITY_POLL,
)
time.sleep(CAPACITY_POLL)
elapsed += CAPACITY_POLL
raise TimeoutError(
f"Capacity wait timed out after {CAPACITY_TIMEOUT}s"
)
# ---------------------------------------------------------------------------
# Log parsing helpers
# ---------------------------------------------------------------------------
def _parse_junit_xml(content: str) -> tuple[dict[str, bool], dict[str, str]]:
try:
root = ET.fromstring(content)
except ET.ParseError:
return {}, {}
results: dict[str, bool] = {}
failures: dict[str, str] = {}
for tc in root.iter("testcase"):
name = tc.get("name", "")
if classname := tc.get("classname", ""):
name = f"{classname}.{name}"
failure_el = tc.find("failure")
if failure_el is None:
failure_el = tc.find("error")
results[name] = failure_el is None
if failure_el is not None:
parts = [failure_el.get("message", ""), failure_el.text or ""]
failures[name] = "\n".join(p for p in parts if p).strip()
return results, failures
def _parse_pytest_output(content: str) -> dict[str, bool]:
results: dict[str, bool] = {}
for m in _PYTEST_LINE_RE.finditer(content):
results[m.group(1)] = m.group(2) == "PASSED"
return results
def fetch_logs_and_parse_tests(
client, job_id: str, max_retries: int = 5, retry_delay: int = 30
) -> tuple[dict[str, bool], dict[str, str], dict[str, str]]:
"""Returns (test_results, raw_logs, failure_details)."""
log_files = None
for attempt in range(1, max_retries + 1):
try:
log_files = qdc_api.get_job_log_files(client, job_id)
break
except Exception as e:
if attempt < max_retries:
log.warning(
"get_job_log_files failed (attempt %d/%d): %s — retrying in %ds",
attempt, max_retries, e, retry_delay,
)
time.sleep(retry_delay)
else:
log.error(
"get_job_log_files failed after %d attempts: %s", max_retries, e
)
return {}, {}, {}
if not log_files:
log.warning("No log files returned for job %s", job_id)
return {}, {}, {}
test_results: dict[str, bool] = {}
pytest_fallback: dict[str, bool] = {}
raw_logs: dict[str, str] = {}
failure_details: dict[str, str] = {}
with tempfile.TemporaryDirectory() as tmpdir:
for lf in log_files:
zip_path = os.path.join(tmpdir, "log.zip")
log.info("Downloading log file: %s", lf.filename)
qdc_api.download_job_log_files(client, lf.filename, zip_path)
try:
shutil.unpack_archive(zip_path, tmpdir, "zip")
except Exception as e:
log.warning("Could not unpack %s as zip: %s", lf.filename, e)
for root_dir, _, files in os.walk(tmpdir):
for fname in sorted(files):
fpath = os.path.join(root_dir, fname)
content = Path(fpath).read_text(errors="replace")
if fname.endswith(".xml"):
results, failures = _parse_junit_xml(content)
test_results.update(results)
failure_details.update(failures)
elif fname.endswith(".log"):
if fname in _EXCLUDED_LOGS:
continue
log.info("--- %s ---\n%s", fname, content)
raw_logs[fname] = content
pytest_fallback.update(_parse_pytest_output(content))
return (
(test_results if test_results else pytest_fallback),
raw_logs,
failure_details,
)
def write_summary(result: JobResult, title: str = "QDC Test Results") -> None:
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if not summary_path:
return
icon = "✅" if result.passed else "❌"
lines = [
f"## {title}\n",
f"Overall: {icon} {'PASSED' if result.passed else 'FAILED'}\n",
]
reportable = {n: ok for n, ok in result.tests.items() if "test_install" not in n}
if reportable:
lines += ["| Test | Result |", "| ---- | ------ |"]
for name, ok in reportable.items():
lines.append(f"| `{name}` | {'✅' if ok else '❌'} |")
passed_n = sum(1 for v in reportable.values() if v)
failed_n = sum(1 for v in reportable.values() if not v)
lines += ["", f"**{passed_n} passed, {failed_n} failed**"]
else:
lines.append("_No per-test data available._")
failed_names = [n for n, ok in reportable.items() if not ok]
if failed_names:
lines += ["", "### Failures"]
for name in failed_names:
detail = result.failure_details.get(name)
if detail:
lines += [
f"<details><summary><code>{name}</code></summary>",
"",
"```",
detail,
"```",
"",
"</details>",
]
if result.raw_logs:
lines += ["", "### Raw Logs"]
for fname, content in sorted(result.raw_logs.items()):
lines += [
f"<details><summary>{fname}</summary>",
"",
"```",
content.rstrip(),
"```",
"",
"</details>",
]
with open(summary_path, "a") as f:
f.write("\n".join(lines) + "\n")
# =============================================================================
# CLI + main
# =============================================================================
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument("--pkg-dir", required=True, type=Path,
help="Installed llama.cpp package directory (contains bin/ and lib/)")
p.add_argument("--model-url",
help="Direct URL to the GGUF model file (required for --test bench)")
p.add_argument("--device", required=True,
help="QDC chipset name, e.g. SM8750")
p.add_argument("--test", choices=["bench", "backend-ops", "all"], default="bench",
help="Test suite to run (default: bench)")
p.add_argument("--job-timeout", type=int, default=JOB_TIMEOUT, metavar="SECONDS",
help=f"Max seconds to wait for job completion (default: {JOB_TIMEOUT})")
p.add_argument("--retries", type=int, default=DEFAULT_RETRIES, metavar="N",
help="Number of retries when device is unavailable (default: 0)")
p.add_argument("--retry-delay", type=int, default=RETRY_DELAY, metavar="SECONDS",
help=f"Seconds to wait between retries (default: {RETRY_DELAY})")
args = p.parse_args()
if args.test in ("bench", "all") and not args.model_url:
p.error("--model-url is required when --test bench or --test all")
return args
def _submit_and_run_job(client, args, spec, target_id, artifact_id) -> JobResult:
"""Submit a QDC job and wait for results.
Raises DeviceUnavailableError for transient device/resource issues that
are worth retrying. Returns JobResult for definitive outcomes (pass or
test failure).
"""
try:
wait_for_capacity(client)
except TimeoutError:
raise DeviceUnavailableError("Capacity wait timed out — device busy")
job_name = spec.job_name_fmt.format(base="llama.cpp Hexagon tests")
job_id = qdc_api.submit_job(
public_api_client=client,
target_id=target_id,
job_name=job_name,
external_job_id=None,
job_type=JobType.AUTOMATED,
job_mode=JobMode.APPLICATION,
timeout=max(1, args.job_timeout // 60),
test_framework=spec.test_framework,
entry_script=spec.entry_script,
job_artifacts=[artifact_id],
monkey_events=None,
monkey_session_timeout=None,
job_parameters=[JobSubmissionParameter.WIFIENABLED],
)
if job_id is None:
raise DeviceUnavailableError("Job submission failed — device may be unavailable")
log.info("Job submitted: %s (device=%s)", job_id, args.device)
try:
job_status = wait_for_job(client, job_id, timeout=args.job_timeout)
except TimeoutError as e:
raise DeviceUnavailableError(str(e))
log.info("Job %s finished: %s", job_id, job_status)
wait_for_log_upload(client, job_id)
tests, raw_logs, failure_details = fetch_logs_and_parse_tests(client, job_id)
job_ok = job_status == JobState.COMPLETED.value.lower()
if not job_ok and not tests:
raise DeviceUnavailableError(
f"Job did not complete (status={job_status}) and produced no test results"
)
passed = job_ok and all(tests.values()) if tests else job_ok
if spec.test_framework == TestFramework.BASH and not tests:
log.error("No test results recovered (state=%s). Script likely never ran.", job_status)
passed = False
if not passed:
log.error("Job did not complete successfully or tests failed (status=%s)", job_status)
return JobResult(passed=passed, tests=tests, raw_logs=raw_logs, failure_details=failure_details)
def main() -> int:
args = parse_args()
platform = DEVICE_PLATFORM.get(args.device)
if platform is None:
log.error(
"Unknown device %r. Known: %s",
args.device, ", ".join(sorted(DEVICE_PLATFORM.keys())),
)
return 1
spec = PLATFORM_SPECS[platform]
api_key = os.environ.get("QDC_API_KEY")
if not api_key:
log.error("QDC_API_KEY environment variable must be set")
return 1
if not args.pkg_dir.is_dir():
log.error("--pkg-dir %s does not exist", args.pkg_dir)
return 1
client = qdc_api.get_public_api_client_using_api_key(
api_key_header=api_key,
app_name_header="llama-cpp-ci",
on_behalf_of_header="llama-cpp-ci",
client_type_header="Python",
)
target_id = qdc_api.get_target_id(client, args.device)
if target_id is None:
log.error("Could not find QDC target for device %r", args.device)
return 1
with tempfile.TemporaryDirectory() as tmpdir:
log.info("Building %s artifact (test=%s) ...", platform.value, args.test)
zip_path = spec.build_artifact(
args.pkg_dir, Path(tmpdir), args.test, args.model_url
)
log.info("Uploading artifact (%d MB) ...", zip_path.stat().st_size // 1_000_000)
artifact_id = qdc_api.upload_file(client, str(zip_path), ArtifactType.TESTSCRIPT)
if artifact_id is None:
log.error("Artifact upload failed")
return 1
max_attempts = 1 + args.retries
for attempt in range(1, max_attempts + 1):
try:
result = _submit_and_run_job(client, args, spec, target_id, artifact_id)
break
except DeviceUnavailableError as e:
if attempt < max_attempts:
log.warning(
"Attempt %d/%d failed (device unavailable): %s — retrying in %ds",
attempt, max_attempts, e, args.retry_delay,
)
time.sleep(args.retry_delay)
else:
log.error(
"Attempt %d/%d failed (device unavailable): %s — no retries left",
attempt, max_attempts, e,
)
write_summary(
JobResult(passed=False, tests={}),
title=f"QDC Device Unavailable ({args.device})",
)
return 1
else:
return 1
if args.test == "backend-ops":
title = f"Backend Ops — HTP0 ({args.device})"
elif args.test == "all":
title = f"QDC Tests ({args.device})"
else:
title = f"QDC Test Results ({args.device})"
write_summary(result, title=title)
return 0 if result.passed else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,20 @@
"""Shared pytest fixtures for QDC on-device test runners."""
import os
import pytest
from appium import webdriver
from utils import options, write_qdc_log
@pytest.fixture(scope="session", autouse=True)
def driver():
return webdriver.Remote(command_executor="http://127.0.0.1:4723/wd/hub", options=options)
def pytest_sessionfinish(session, exitstatus):
xml_path = getattr(session.config.option, "xmlpath", None) or "results.xml"
if os.path.exists(xml_path):
with open(xml_path) as f:
write_qdc_log("results.xml", f.read())
@@ -0,0 +1,234 @@
#!/bin/bash
# llama.cpp Hexagon test entry script for QDC Linux IoT (BASH framework).
#
# Placeholders substituted by run_qdc_jobs.py (--platform linux) before upload:
# {MODEL_URL} direct URL to a .gguf model file
# {TEST_MODE} bench | backend-ops | all
#
# QDC extracts the artifact zip to /data/local/tmp/TestContent/ and invokes
# this script via: /bin/bash /data/local/tmp/TestContent/run_linux.sh
# Any files written under /data/local/tmp/QDC_logs/ are auto-uploaded.
set +e
umask 022
LOG_DIR=/data/local/tmp/QDC_logs
BUNDLE_DIR=/data/local/tmp/TestContent/llama_cpp_bundle
MODEL_DIR=/data/local/tmp/gguf
MODEL_PATH="$MODEL_DIR/model.gguf"
RESULTS_XML="$LOG_DIR/results.xml"
mkdir -p "$LOG_DIR" "$MODEL_DIR"
# Redirect all parent-shell output to script.log so QDC auto-uploads it;
# per-case runs still capture their own stdout/stderr into dedicated logs.
exec > "$LOG_DIR/script.log" 2>&1
echo "=== env ==="
date -u
uname -a
pwd
mount -o rw,remount / 2>/dev/null || true
cd "$BUNDLE_DIR" || { echo "FATAL: bundle missing at $BUNDLE_DIR"; exit 1; }
chmod +x bin/* 2>/dev/null
export LD_LIBRARY_PATH="$BUNDLE_DIR/lib:$LD_LIBRARY_PATH"
export ADSP_LIBRARY_PATH="$BUNDLE_DIR/lib"
export GGML_HEXAGON_EXPERIMENTAL=1
echo "=== download model ==="
MODEL_URL="{MODEL_URL}"
if [ -z "$MODEL_URL" ]; then
echo "No model URL provided, skipping download"
elif [ ! -f "$MODEL_PATH" ]; then
curl -L -fS --retry 3 --retry-delay 5 -o "$MODEL_PATH" "$MODEL_URL"
curl_rc=$?
if [ $curl_rc -ne 0 ]; then
echo "FATAL: model download failed (rc=$curl_rc)"
exit 1
fi
ls -la "$MODEL_PATH"
fi
# ---------------------------------------------------------------------------
# JUnit XML helpers
# ---------------------------------------------------------------------------
xml_open() {
printf '%s\n' \
'<?xml version="1.0" encoding="utf-8"?>' \
"<testsuites>" \
"<testsuite name=\"llama_cpp_linux\">" \
> "$RESULTS_XML"
}
xml_close() {
printf '%s\n' '</testsuite>' '</testsuites>' >> "$RESULTS_XML"
}
xml_case_pass() {
local classname=$1 name=$2
printf '<testcase classname="%s" name="%s"/>\n' "$classname" "$name" >> "$RESULTS_XML"
}
xml_case_fail() {
local classname=$1 name=$2 rc=$3 logfile=$4
{
printf '<testcase classname="%s" name="%s">\n' "$classname" "$name"
printf '<failure message="exit %s"><![CDATA[\n' "$rc"
tail -c 4096 "$logfile" 2>/dev/null | sed 's/]]>/]] >/g'
printf '\n]]></failure>\n</testcase>\n'
} >> "$RESULTS_XML"
}
# Map backend name -> "NDEV --device" pair. "none" means no offload (CPU).
backend_env() {
case "$1" in
cpu) echo "0 none" ;;
gpu) echo "0 GPUOpenCL" ;;
npu) echo "1 HTP0" ;;
esac
}
backend_log_name() {
case "$1" in
cpu) echo "cpu" ;;
gpu) echo "gpu" ;;
npu) echo "htp" ;;
esac
}
backend_device_name() {
case "$1" in
cpu) echo "none" ;;
gpu) echo "GPUOpenCL" ;;
npu) echo "HTP0" ;;
esac
}
# Append a diagnostic block when a per-case `timeout N` fires (rc=124). The
# naked log file at that point usually just ends mid-OpenCL-init with no
# stderr, which is hard to read in CI summaries.
note_timeout_if_triggered() {
local rc=$1 budget=$2 log=$3
[ "$rc" -eq 124 ] || return 0
{
printf '\n'
printf '=== TIMEOUT after %ss ===\n' "$budget"
printf 'uptime: '; uptime 2>/dev/null
printf 'free -m:\n'; free -m 2>/dev/null
printf 'loadavg: '; cat /proc/loadavg 2>/dev/null
} >> "$log"
}
completion_extra_args() {
case "$1" in
cpu) echo "--device none --ctx-size 2048 -no-cnv -n 32 --seed 42" ;;
gpu) echo "--device GPUOpenCL --ctx-size 2048 -no-cnv -n 32 --seed 42" ;;
npu) echo "--device HTP0 --ctx-size 2048 -no-cnv -n 32 --seed 42 --ubatch-size 1024" ;;
esac
}
run_completion_case() {
local name=$1
local parts=($(backend_env "$name"))
local ndev=${parts[0]} device=${parts[1]}
local device_log_name=$(backend_device_name "$name")
local log="$LOG_DIR/llama_completion_${device_log_name}.log"
local prompt="$LOG_DIR/bench_prompt.txt"
echo 'What is the capital of France?' > "$prompt"
local extra
extra=$(completion_extra_args "$name")
echo "=== [completion:$name] llama-completion --device $device (NDEV=$ndev) ==="
timeout 600 env GGML_HEXAGON_NDEV=$ndev ./bin/llama-completion \
-m "$MODEL_PATH" \
-f "$prompt" \
$extra \
> "$log" 2>&1 < /dev/null
local rc=$?
note_timeout_if_triggered "$rc" 600 "$log"
if [ $rc -eq 0 ]; then
xml_case_pass "tests.test_bench_posix" "test_llama_completion[$name]"
else
xml_case_fail "tests.test_bench_posix" "test_llama_completion[$name]" "$rc" "$log"
fi
}
run_bench_case() {
local name=$1
local parts=($(backend_env "$name"))
local ndev=${parts[0]} device=${parts[1]}
local log_suffix=$(backend_log_name "$name")
local log="$LOG_DIR/llama_bench_${log_suffix}.log"
local ubatch_arg=""
[ "$name" = "npu" ] && ubatch_arg="--ubatch-size 1024"
echo "=== [bench:$name] llama-bench --device $device (NDEV=$ndev) ==="
timeout 600 env GGML_HEXAGON_NDEV=$ndev ./bin/llama-bench \
-m "$MODEL_PATH" \
--device "$device" \
-ngl 99 \
$ubatch_arg \
-t 4 \
-p 128 \
-n 32 \
> "$log" 2>&1
local rc=$?
note_timeout_if_triggered "$rc" 600 "$log"
if [ $rc -eq 0 ]; then
xml_case_pass "tests.test_bench_posix" "test_llama_bench[$name]"
else
xml_case_fail "tests.test_bench_posix" "test_llama_bench[$name]" "$rc" "$log"
fi
}
run_backend_ops_case() {
local dtype=$1
local log="$LOG_DIR/backend_ops_${dtype}.log"
local pattern
case "$dtype" in
q4_0)
# Matches Android: exclude a known-broken shape on NPU.
pattern='^(?=.*type_a=q4_0)(?!.*type_b=f32,m=576,n=512,k=576).*$'
;;
*)
pattern="type_a=${dtype}"
;;
esac
echo "=== [backend-ops:$dtype] test-backend-ops -b HTP0 -o MUL_MAT ==="
timeout 600 env GGML_HEXAGON_NDEV=1 GGML_HEXAGON_HOSTBUF=0 ./bin/test-backend-ops \
-b HTP0 -o MUL_MAT -p "$pattern" \
> "$log" 2>&1
local rc=$?
note_timeout_if_triggered "$rc" 600 "$log"
if [ $rc -eq 0 ]; then
xml_case_pass "tests.test_backend_ops_posix" "test_backend_ops_htp0[$dtype]"
else
xml_case_fail "tests.test_backend_ops_posix" "test_backend_ops_htp0[$dtype]" "$rc" "$log"
fi
}
xml_open
case "{TEST_MODE}" in
bench)
for b in cpu gpu npu; do run_completion_case "$b"; done
for b in cpu gpu npu; do run_bench_case "$b"; done
;;
backend-ops)
for d in mxfp4 fp16 q4_0; do run_backend_ops_case "$d"; done
;;
all)
for b in cpu gpu npu; do run_completion_case "$b"; done
for b in cpu gpu npu; do run_bench_case "$b"; done
for d in mxfp4 fp16 q4_0; do run_backend_ops_case "$d"; done
;;
*)
echo "FATAL: unsupported TEST_MODE={TEST_MODE}"
;;
esac
xml_close
echo "=== done ==="
# Host parses results.xml to decide pass/fail.
exit 0
@@ -0,0 +1,48 @@
"""
On-device test-backend-ops runner for llama.cpp (HTP0 backend).
On Android: executed by QDC's Appium test framework on the QDC runner.
The runner has ADB access to the allocated device.
On Linux: runs test-backend-ops directly via run_linux.sh (BASH framework).
"""
import os
import sys
import pytest
from utils import (
BIN_PATH,
push_bundle_if_needed,
run_snapdragon,
write_qdc_log,
)
@pytest.fixture(scope="session", autouse=True)
def install(driver):
push_bundle_if_needed(f"{BIN_PATH}/test-backend-ops")
@pytest.mark.parametrize("type_a", ["mxfp4", "fp16", "q4_0"])
def test_backend_ops_htp0(type_a):
if type_a == "q4_0":
pattern = r'^(?=.*type_a=q4_0)(?!.*type_b=f32,m=576,n=512,k=576).*$'
else:
pattern = f"type_a={type_a}"
result = run_snapdragon(
["test-backend-ops", "-b", "HTP0", "-o", "MUL_MAT", "-p", pattern],
)
write_qdc_log(f"backend_ops_{type_a}.log", result.stdout or "")
assert result.returncode == 0, (
f"test-backend-ops type_a={type_a} failed (exit {result.returncode})"
)
if __name__ == "__main__":
ret = pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)])
if os.path.exists("results.xml"):
with open("results.xml") as f:
write_qdc_log("results.xml", f.read())
sys.exit(ret)
@@ -0,0 +1,105 @@
"""
On-device bench and completion test runner for llama.cpp (CPU, GPU, NPU backends).
On Android: calls scripts/snapdragon/run.py on the QDC runner host
(script wraps commands in adb shell internally).
On Linux: runs llama-bench directly via run_linux.sh (BASH framework).
Placeholders replaced at artifact creation time by run_qdc_jobs.py:
<<MODEL_URL>> Direct URL to the GGUF model file (downloaded on-device)
"""
import os
import subprocess
import sys
import pytest
from utils import (
BIN_PATH,
MODEL_DEVICE_PATH,
PROMPT_DIR,
push_bundle_if_needed,
run_adb_command,
run_snapdragon,
write_qdc_log,
)
MODEL_URL = "<<MODEL_URL>>"
@pytest.fixture(scope="session", autouse=True)
def install(driver):
push_bundle_if_needed(f"{BIN_PATH}/llama-cli")
run_adb_command(f"mkdir -p /data/local/tmp/gguf {PROMPT_DIR}")
run_adb_command(f"echo 'What is the capital of France?' > {PROMPT_DIR}/bench_prompt.txt")
check = subprocess.run(
["adb", "shell", f"ls {MODEL_DEVICE_PATH}"],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if check.returncode != 0:
run_adb_command(f'curl -L -J --output {MODEL_DEVICE_PATH} "{MODEL_URL}"')
@pytest.mark.parametrize(
"device",
[
pytest.param("none", id="cpu"),
pytest.param("GPUOpenCL", id="gpu"),
pytest.param("HTP0", id="npu"),
],
)
def test_llama_completion(device):
args = [
"llama-completion",
"-m", MODEL_DEVICE_PATH,
"-f", f"{PROMPT_DIR}/bench_prompt.txt",
"-no-cnv",
"--ctx-size", "8192",
"-n", "128",
"--seed", "42",
]
if device == "HTP0":
args += ["--ubatch-size", "1024"]
result = run_snapdragon(args, device=device)
write_qdc_log(f"llama_completion_{device}.log", result.stdout or "")
assert result.returncode == 0, (
f"llama-completion {device} failed (exit {result.returncode})"
)
_DEVICE_LOG_NAME = {"none": "cpu", "GPUOpenCL": "gpu", "HTP0": "htp"}
@pytest.mark.parametrize(
"device",
[
pytest.param("none", id="cpu"),
pytest.param("GPUOpenCL", id="gpu"),
pytest.param("HTP0", id="npu"),
],
)
def test_llama_bench(device):
args = [
"llama-bench",
"-m", MODEL_DEVICE_PATH,
"-ngl", "99",
"-p", "128",
"-n", "32",
]
if device == "HTP0":
args += ["--ubatch-size", "1024"]
result = run_snapdragon(args, device=device)
write_qdc_log(f"llama_bench_{_DEVICE_LOG_NAME[device]}.log", result.stdout or "")
assert result.returncode == 0, (
f"llama-bench {device} failed (exit {result.returncode})"
)
if __name__ == "__main__":
ret = pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)])
if os.path.exists("results.xml"):
with open("results.xml") as f:
write_qdc_log("results.xml", f.read())
sys.exit(ret)
@@ -0,0 +1,152 @@
"""Shared helpers for QDC on-device test runners."""
from __future__ import annotations
import logging
import os
import subprocess
import sys
import tempfile
from appium.options.common import AppiumOptions
log = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# On-device paths
# ---------------------------------------------------------------------------
BUNDLE_PATH = "/data/local/tmp/llama.cpp"
BIN_PATH = f"{BUNDLE_PATH}/bin"
LIB_PATH = f"{BUNDLE_PATH}/lib"
QDC_LOGS_PATH = "/data/local/tmp/QDC_logs"
SCRIPTS_DIR = "/qdc/appium"
MODEL_NAME = "model.gguf"
MODEL_DEVICE_PATH = "/data/local/tmp/gguf/model.gguf"
PROMPT_DIR = "/data/local/tmp/scorecard_prompts"
# ---------------------------------------------------------------------------
# Appium session options
# ---------------------------------------------------------------------------
options = AppiumOptions()
options.set_capability("automationName", "UiAutomator2")
options.set_capability("platformName", "Android")
options.set_capability("deviceName", os.getenv("ANDROID_DEVICE_VERSION"))
# ---------------------------------------------------------------------------
# Shell / process helpers
# ---------------------------------------------------------------------------
def write_qdc_log(filename: str, content: str) -> None:
"""Write content as a log file for QDC log collection."""
subprocess.run(
["adb", "shell", f"mkdir -p {QDC_LOGS_PATH}"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as f:
f.write(content)
tmp_path = f.name
try:
subprocess.run(
["adb", "push", tmp_path, f"{QDC_LOGS_PATH}/{filename}"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
finally:
os.unlink(tmp_path)
def ensure_bundle(check_binary: str | None = None) -> None:
"""Ensure the llama_cpp_bundle is available on the target device."""
push_bundle_if_needed(check_binary or f"{BIN_PATH}/llama-cli")
# ---------------------------------------------------------------------------
# Android / Linux host helpers
# ---------------------------------------------------------------------------
def run_adb_command(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
"""Run a command on-device via ``adb shell`` with exit-code sentinel."""
raw = subprocess.run(
["adb", "shell", f"{cmd}; echo __RC__:$?"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout = raw.stdout
returncode = raw.returncode
if stdout:
lines = stdout.rstrip("\n").split("\n")
if lines and lines[-1].startswith("__RC__:"):
try:
returncode = int(lines[-1][7:])
stdout = "\n".join(lines[:-1]) + "\n"
except ValueError:
pass
log.info(stdout)
result = subprocess.CompletedProcess(raw.args, returncode, stdout=stdout)
if check:
assert returncode == 0, f"Command failed (exit {returncode})"
return result
def run_snapdragon(
cmd_args: list[str],
*,
device: str | None = None,
extra_run_args: list[str] | None = None,
extra_env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess:
"""Run a tool via scripts/snapdragon/run.py targeting android."""
env = os.environ.copy()
env["GGML_HEXAGON_EXPERIMENTAL"] = "1"
if extra_env:
env.update(extra_env)
cmd = [sys.executable, f"{SCRIPTS_DIR}/run.py", "--target", "android"]
if device is not None:
cmd.extend(["-d", device])
if extra_run_args:
cmd.extend(extra_run_args)
cmd.append("--")
cmd.extend(cmd_args)
result = subprocess.run(
cmd, env=env,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
log.info(result.stdout)
return result
def adb_shell(cmd: str) -> None:
"""Run a command via adb shell (fire-and-forget, no error check)."""
subprocess.run(
["adb", "shell", "sh", "-c", cmd],
capture_output=True, encoding="utf-8", errors="replace", check=False,
)
def push_bundle_if_needed(check_binary: str) -> None:
"""Push llama_cpp_bundle to the device if check_binary is not already present."""
result = subprocess.run(
["adb", "shell", f"ls {check_binary}"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if result.returncode != 0:
subprocess.run(
["adb", "push", "/qdc/appium/llama_cpp_bundle/", BUNDLE_PATH],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
subprocess.run(
["adb", "shell", f"find {BUNDLE_PATH}/bin -type f -exec chmod 755 {{}} +"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
+569
View File
@@ -0,0 +1,569 @@
#!/usr/bin/env python3
#
# Run llama.cpp tools on Snapdragon devices (natively, via ADB, or SSH).
#
import sys
import os
import argparse
import subprocess
import platform
import shlex
import logging
logger = logging.getLogger("run")
MANAGED_ENV_NAMES = (
"GGML_HEXAGON_DEVICES",
"GGML_HEXAGON_VERBOSE",
"GGML_HEXAGON_PROFILE",
"GGML_HEXAGON_NHVX",
"GGML_HEXAGON_NHMX",
"GGML_HEXAGON_HOSTBUF",
"GGML_HEXAGON_DMA64",
"GGML_HEXAGON_OPBATCH",
"GGML_HEXAGON_OPQUEUE",
"GGML_HEXAGON_OPPOLL",
"GGML_HEXAGON_OPFILTER",
"GGML_HEXAGON_OPFUSION",
"GGML_HEXAGON_RT_REPACK",
"GGML_HEXAGON_VMEM",
"GGML_HEXAGON_MBUF",
"GGML_HEXAGON_MM_SELECT",
"GGML_HEXAGON_FA_SELECT",
"GGML_HEXAGON_FA_HEAD_SPLIT",
"GGML_HEXAGON_GDN_SELECT",
"GGML_HEXAGON_AR_SELECT",
"GGML_HEXAGON_AR_SCATTER",
"GGML_HEXAGON_ETM",
"GGML_HEXAGON_ARCH",
"GGML_HEXAGON_OPTRACE",
"GGML_OPENCL_PLATFORM",
"GGML_OPENCL_DEVICE",
"GGML_OPENCL_OPFILTER",
"GGML_OPENCL_KERNEL_CACHE_DIR",
"GGML_OPENCL_KERNEL_CACHE_DEBUG",
"GGML_OPENCL_FA_TUNE",
"GGML_OPENCL_DISABLE_FUSION",
"GGML_OPENCL_ADRENO_XMEM_GEMM",
"GGML_OPENCL_ADRENO_USE_LARGE_BUFFER",
"GGML_SCHED_DEBUG",
"ADSP_LIBRARY_PATH",
"CDSP_LIBRARY_PATH",
"CDSP1_LIBRARY_PATH",
"CDSP2_LIBRARY_PATH",
"CDSP3_LIBRARY_PATH",
"MTMD_BACKEND_DEVICE",
"D",
"DEVICE",
)
DSP_LIBRARY_ENV_NAMES = (
"ADSP_LIBRARY_PATH",
"CDSP_LIBRARY_PATH",
"CDSP1_LIBRARY_PATH",
"CDSP2_LIBRARY_PATH",
"CDSP3_LIBRARY_PATH",
)
def parse_target(target_str):
if not target_str:
return None, None, None
parts = target_str.split(":")
target_name = parts[0]
if target_name in ("adb", "android"):
serial = ":".join(parts[1:]) if len(parts) > 1 else None
return "android", serial, "adb"
elif target_name in ("lnx", "linux", "ubuntu"):
if len(parts) > 1 and parts[1] == "adb":
serial = ":".join(parts[2:]) if len(parts) > 2 else None
return "linux", serial, "adb"
host = ":".join(parts[1:]) if len(parts) > 1 else None
return "linux", host, "ssh"
elif target_str in ("wos", "windows"):
return "windows", None, None
else:
return None, None, None
def shlex_join(args_list):
if hasattr(shlex, 'join'):
return shlex.join(args_list)
import pipes
return " ".join(pipes.quote(x) for x in args_list)
def default_target_dir(target_type, target_transport, home_dir):
if target_type == "android":
return "/data/local/tmp/llama.cpp"
if target_type == "linux":
if home_dir:
return home_dir.rstrip("/") + "/llama.cpp"
if target_transport == "adb":
return "/home/llama.cpp"
return "~/llama.cpp"
return "~/llama.cpp"
def library_env_parts(lib_dir):
env_parts = [f"LD_LIBRARY_PATH={lib_dir}"]
for name in DSP_LIBRARY_ENV_NAMES:
env_parts.append(f"{name}={lib_dir}")
return env_parts
def adb_root(adb_base):
root_cmd = adb_base + ["root"]
logger.info(f"+ {' '.join(root_cmd)}")
res = subprocess.run(root_cmd)
if res.returncode != 0:
logger.error("ADB root failed.")
sys.exit(res.returncode)
wait_cmd = adb_base + ["wait-for-device"]
logger.info(f"+ {' '.join(wait_cmd)}")
res = subprocess.run(wait_cmd)
if res.returncode != 0:
logger.error("ADB wait-for-device failed.")
sys.exit(res.returncode)
def split_device_list(devices):
parts = []
curr = []
bracket_depth = 0
for ch in devices:
if ch == '[':
bracket_depth += 1
curr.append(ch)
elif ch == ']':
if bracket_depth > 0:
bracket_depth -= 1
curr.append(ch)
elif ch == ',' and bracket_depth == 0:
part = "".join(curr).strip()
if part:
parts.append(part)
curr = []
else:
curr.append(ch)
part = "".join(curr).strip()
if part:
parts.append(part)
return parts
def device_arg_from_devices(devices):
if devices.isdigit():
n = int(devices)
return ",".join(f"HTP{i}" for i in range(n))
names = []
for part in split_device_list(devices):
if "[" in part:
part = part.split("[", 1)[0].strip()
if part:
names.append(part)
return ",".join(names)
def normalize_cmd_device_args(cmd_args):
for i, arg in enumerate(cmd_args):
if arg == "--device" and i + 1 < len(cmd_args):
cmd_args[i + 1] = device_arg_from_devices(cmd_args[i + 1])
elif arg.startswith("--device="):
cmd_args[i] = "--device=" + device_arg_from_devices(arg.split("=", 1)[1])
def main():
logging.basicConfig(level=logging.INFO, format='%(message)s')
# Split arguments at '--'
if '--' in sys.argv:
idx = sys.argv.index('--')
run_args = sys.argv[1:idx]
cmd_args = sys.argv[idx + 1:]
else:
run_args = sys.argv[1:]
cmd_args = []
parser = argparse.ArgumentParser(
description="Unified runner for llama.cpp tools on Snapdragon (natively, via ADB, or via SSH)."
)
parser.add_argument("--target", help="Execution target (e.g. android[:serial]/adb[:serial], ubuntu:adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, windows/wos) (default: local run)")
parser.add_argument("--target-dir", help="Target directory on the device (default: /data/local/tmp/llama.cpp for Android, ~/llama.cpp for Linux, /home/llama.cpp for Linux via ADB)")
parser.add_argument("--home-dir", help="Home directory for Linux targets when deriving the default target directory")
parser.add_argument("--install-dir", help="Install directory name (defaults to pkg-TARGET or pkg-TARGET-dbg prefix based on target)")
parser.add_argument("--debug", action="store_true", help="Use debug build (defaults to pkg-TARGET-dbg folder)")
parser.add_argument("--devices", "--device", "-d", help="Select execution devices (split into NPU and OpenCL GPUs automatically, default: HTP0)")
parser.add_argument("--verbose", help="Verbose level (enables both Hexagon and OpenCL kernel cache debugging)")
parser.add_argument("--profile", help="Profiling flag (enables Hexagon profiling and OpenCL autotuning)")
parser.add_argument("--sched-debug", action="store_true", help="Enable GGML/llama.cpp scheduler debug output (GGML_SCHED_DEBUG=2)")
parser.add_argument("--mtmd-device", help="Specify the backend device ID for Multi-Threaded Multi-Device setup (MTMD_BACKEND_DEVICE)")
parser.add_argument("--no-embd-offload", action="store_true", help="Keep token embeddings and output projection on CPU (-ot token_embd.weight=CPU,output.weight=CPU)")
# Hexagon specific parameters
parser.add_argument("--hex-verbose", help="Enable verbose logging (GGML_HEXAGON_VERBOSE)")
parser.add_argument("--hex-profile", help="Enable NPU/Hexagon profiling and performance metrics print (GGML_HEXAGON_PROFILE)")
parser.add_argument("--hex-nhvx", help="Number of HVX units to use (GGML_HEXAGON_NHVX)")
parser.add_argument("--hex-nhmx", help="Number of HMX units to use. 0 disables HMX power-up (GGML_HEXAGON_NHMX)")
parser.add_argument("--hex-power-mode", help="HTP DCVS/HMX power mode: low_power_saver, power_saver, high_power_saver, low_balanced, balanced, high_performance, sustained_high_performance, burst (GGML_HEXAGON_POWER_MODE)")
parser.add_argument("--hex-hostbuf", help="Enable host buffers (GGML_HEXAGON_HOSTBUF)")
parser.add_argument("--hex-dma64", nargs="?", const="1", help="Enable (1) or disable (0) 64-bit DMA for model weights (GGML_HEXAGON_DMA64)")
parser.add_argument("--hex-opbatch", help="Maximum number of operations to batch into a single HTP execution (GGML_HEXAGON_OPBATCH)")
parser.add_argument("--hex-opqueue", help="Size of the asynchronous NPU operation queue (GGML_HEXAGON_OPQUEUE)")
parser.add_argument("--hex-oppoll", default="1", help="Enable (1) or Disable (0) polling for NPU opbatch completion (GGML_HEXAGON_OPPOLL) (default: 1)")
parser.add_argument("--hex-opfilter", help="Regex pattern to filter/select which operators are offloaded to NPU (GGML_HEXAGON_OPFILTER)")
parser.add_argument("--hex-opfusion", help="NPU graph node fusion optimization level (0: disabled, 1: enabled) (GGML_HEXAGON_OPFUSION)")
parser.add_argument("--hex-rt-repack", help="Runtime weight repacking mode (0: disabled, 1: quantized only, 2: always) (GGML_HEXAGON_RT_REPACK)")
parser.add_argument("--hex-vmem", help="Maximum NPU VMEM size limit in MB to allocate (GGML_HEXAGON_VMEM)")
parser.add_argument("--hex-mbuf", help="Host buffer size limits in MB (supports K/M/G suffix): <dyn>[,<static>[,<total>]] (default: 512,1024,0) (GGML_HEXAGON_MBUF)")
parser.add_argument("--hex-mm-select", help="Select MUL_MAT and MUL_MAT_ID kernel (GGML_HEXAGON_MM_SELECT) 2:HMX,1:HVX,0:disable")
parser.add_argument("--hex-fa-select", help="Select Flash Attention kernel (GGML_HEXAGON_FA_SELECT) 2:HMX,1:HVX,0:disable")
parser.add_argument("--hex-fa-head-split", help="Enable (1) or disable (0) head-parallel flash_attn partitioning (GGML_HEXAGON_FA_HEAD_SPLIT)")
parser.add_argument("--hex-gdn-select", help="Select Gated Delta Net kernel (GGML_HEXAGON_GDN_SELECT) 2:HMX,1:HVX,0:disable")
parser.add_argument("--hex-ar-select", help="Select All-Reduce kernel (GGML_HEXAGON_AR_SELECT) 1:enable,0:disable")
parser.add_argument("--hex-ar-scatter", help="Enable (1) or disable (0) reduce-scatter for fused ALLREDUCE+ADD (GGML_HEXAGON_AR_SCATTER)")
parser.add_argument("--hex-etm", help="Enable Embedded Trace Macrocell hardware tracing / trace logging (GGML_HEXAGON_ETM)")
parser.add_argument("--hex-arch", help="Target Hexagon NPU architecture version override (v73, v75, v79, v81, etc.) (GGML_HEXAGON_ARCH)")
parser.add_argument("--hex-optrace", help="Trace buffer size in number of records (GGML_HEXAGON_OPTRACE)")
# OpenCL specific parameters
parser.add_argument("--cl-platform", help="Select OpenCL platform name/regex (e.g. Qualified Qualcomm OpenCL platform) (GGML_OPENCL_PLATFORM)")
parser.add_argument("--cl-device", help="Select OpenCL device name/regex (e.g. Adreno GPU) (GGML_OPENCL_DEVICE)")
parser.add_argument("--cl-opfilter", help="Regex pattern to filter/select which operators are offloaded to OpenCL (GGML_OPENCL_OPFILTER)")
parser.add_argument("--cl-disable-fusion", action="store_true", help="Disable OpenCL kernel fusion optimizations (GGML_OPENCL_DISABLE_FUSION)")
parser.add_argument("--cl-cache-dir", help="Directory path to store compiled OpenCL program binaries (GGML_OPENCL_KERNEL_CACHE_DIR)")
parser.add_argument("--cl-cache-debug", help="Enable verbose debugging logs for the kernel caching system (GGML_OPENCL_KERNEL_CACHE_DEBUG)")
parser.add_argument("--cl-fa-tune", action="store_true", help="Enable automatic Flash Attention kernel autotuning (GGML_OPENCL_FA_TUNE)")
parser.add_argument("--cl-adreno-xmem", action="store_true", help="Enforce matmul using texture/image (xmem) memory paths on Adreno GPUs (GGML_OPENCL_ADRENO_XMEM_GEMM)")
parser.add_argument("--cl-adreno-large-buffer", action="store_true", help="Allow allocating larger buffer sizes on Adreno GPUs (GGML_OPENCL_ADRENO_USE_LARGE_BUFFER)")
args = parser.parse_args(run_args)
if not cmd_args:
parser.print_help()
logger.error("\nError: No command specified after '--'")
sys.exit(1)
target_type = None
target_val = None
target_transport = None
target_prefix = None
if args.target:
target_type, target_val, target_transport = parse_target(args.target)
if not target_type:
logger.error(f"Error: Invalid target format '{args.target}'. Must be android[:serial]/adb[:serial], ubuntu:adb[:serial], linux:[user@]host/lnx:[user@]host/ubuntu:[user@]host, or windows/wos.")
sys.exit(1)
target_prefix = args.target.split(":", 1)[0]
# Resolve install directory
install_dir = args.install_dir
if not install_dir:
if target_prefix:
suffix = "-dbg" if args.debug else ""
install_dir = f"pkg-{target_prefix}{suffix}"
else:
# Smart branch folder detection for local run if default is not set
prefixes = ("wos", "windows", "lnx", "linux", "ubuntu", "adb", "android")
suffixes = ("-dbg", "") if args.debug else ("", "-dbg")
found = False
for suffix in suffixes:
for prefix in prefixes:
test_path = f"./pkg-{prefix}{suffix}/llama.cpp"
if os.path.exists(test_path):
install_dir = f"pkg-{prefix}{suffix}"
found = True
break
if found:
break
if not install_dir:
install_dir = "pkg-android" # Fallback default
# Host side package path
package_path = os.path.join(install_dir, "llama.cpp")
# Environment variables to map
env_vars = {}
def set_env(env_name, opt_val):
if opt_val is not None:
env_vars[env_name] = str(opt_val)
# Resolve and filter devices (HTP vs OpenCL)
device_in_cmd = None
for i, arg in enumerate(cmd_args):
if arg == "--device" and i + 1 < len(cmd_args):
device_in_cmd = cmd_args[i + 1]
break
elif arg.startswith("--device="):
device_in_cmd = arg.split("=", 1)[1]
break
if args.devices is not None:
devices_val = args.devices
elif device_in_cmd is not None:
devices_val = device_in_cmd
else:
devices_val = "HTP0"
if devices_val.isdigit():
hex_devices = devices_val
cl_device = ""
else:
parts = split_device_list(devices_val)
# Any device containing "htp" is Hexagon, rest is OpenCL
hex_parts = [p for p in parts if "htp" in p.lower()]
cl_parts = [
p for p in parts
if "htp" not in p.lower()
and p.lower() not in ("none", "cpu")
and not p.lower().startswith("gpuopencl")
]
hex_devices = ",".join(hex_parts)
cl_device = ",".join(cl_parts)
# Set Hexagon devices
if hex_devices:
env_vars["GGML_HEXAGON_DEVICES"] = hex_devices
normalize_cmd_device_args(cmd_args)
# Set OpenCL device (unless overridden by --cl-device)
final_cl_device = args.cl_device if args.cl_device is not None else cl_device
if final_cl_device:
env_vars["GGML_OPENCL_DEVICE"] = final_cl_device
# Map shared & backend-specific parameters with correct overrides
# Verbose logging mapping
hex_verbose_val = args.hex_verbose if args.hex_verbose is not None else args.verbose
set_env("GGML_HEXAGON_VERBOSE", hex_verbose_val)
cl_cache_debug_val = args.cl_cache_debug if args.cl_cache_debug is not None else args.verbose
set_env("GGML_OPENCL_KERNEL_CACHE_DEBUG", cl_cache_debug_val)
# Profiling mapping
hex_profile_val = args.hex_profile if args.hex_profile is not None else args.profile
set_env("GGML_HEXAGON_PROFILE", hex_profile_val)
if args.cl_fa_tune or args.profile is not None:
env_vars["GGML_OPENCL_FA_TUNE"] = "1"
# Other Hexagon environment variables
set_env("GGML_HEXAGON_NHVX", args.hex_nhvx)
set_env("GGML_HEXAGON_NHMX", args.hex_nhmx)
set_env("GGML_HEXAGON_POWER_MODE", args.hex_power_mode)
set_env("GGML_HEXAGON_HOSTBUF", args.hex_hostbuf)
set_env("GGML_HEXAGON_DMA64", args.hex_dma64)
set_env("GGML_HEXAGON_OPBATCH", args.hex_opbatch)
set_env("GGML_HEXAGON_OPQUEUE", args.hex_opqueue)
set_env("GGML_HEXAGON_OPPOLL", args.hex_oppoll)
set_env("GGML_HEXAGON_OPFILTER", args.hex_opfilter)
set_env("GGML_HEXAGON_OPFUSION", args.hex_opfusion)
set_env("GGML_HEXAGON_RT_REPACK", args.hex_rt_repack)
set_env("GGML_HEXAGON_VMEM", args.hex_vmem)
set_env("GGML_HEXAGON_MBUF", args.hex_mbuf)
set_env("GGML_HEXAGON_MM_SELECT", args.hex_mm_select)
set_env("GGML_HEXAGON_FA_SELECT", args.hex_fa_select)
set_env("GGML_HEXAGON_FA_HEAD_SPLIT", args.hex_fa_head_split)
set_env("GGML_HEXAGON_GDN_SELECT", args.hex_gdn_select)
set_env("GGML_HEXAGON_AR_SELECT", args.hex_ar_select)
set_env("GGML_HEXAGON_AR_SCATTER", args.hex_ar_scatter)
set_env("GGML_HEXAGON_ETM", args.hex_etm)
set_env("GGML_HEXAGON_ARCH", args.hex_arch)
set_env("GGML_HEXAGON_OPTRACE", args.hex_optrace)
set_env("MTMD_BACKEND_DEVICE", args.mtmd_device)
# OpenCL environment variables
set_env("GGML_OPENCL_PLATFORM", args.cl_platform)
set_env("GGML_OPENCL_OPFILTER", args.cl_opfilter)
set_env("GGML_OPENCL_KERNEL_CACHE_DIR", args.cl_cache_dir)
if args.cl_disable_fusion:
env_vars["GGML_OPENCL_DISABLE_FUSION"] = "1"
if args.cl_adreno_xmem:
env_vars["GGML_OPENCL_ADRENO_XMEM_GEMM"] = "1"
if args.cl_adreno_large_buffer:
env_vars["GGML_OPENCL_ADRENO_USE_LARGE_BUFFER"] = "1"
if args.sched_debug:
env_vars["GGML_SCHED_DEBUG"] = "2"
# Resolve executable path
executable = cmd_args[0]
known_binaries = ["llama-cli", "llama-bench", "llama-completion", "llama-mtmd-cli", "test-backend-ops"]
if executable in known_binaries:
if target_type in ("android", "linux"):
resolved_exec = f"./bin/{executable}"
else:
if platform.system() == "Windows":
resolved_exec = os.path.normpath(os.path.join(package_path, "bin", f"{executable}.exe"))
else:
resolved_exec = os.path.normpath(os.path.join(package_path, "bin", executable))
cmd_args[0] = resolved_exec
# Infer device string to pass to the tool
basename = os.path.basename(executable)
if basename.endswith(".exe"):
basename = basename[:-4]
device_val = None
if basename == "test-backend-ops":
for i in range(len(cmd_args)):
if cmd_args[i] in ("-p", "--params") and i + 1 < len(cmd_args):
val = cmd_args[i + 1]
new_val = ""
for j, char in enumerate(val):
if char in ('[', ']'):
if j > 0 and val[j - 1] == '\\':
new_val += char
else:
new_val += '\\' + char
else:
new_val += char
cmd_args[i + 1] = new_val
has_b = any(arg == "-b" for arg in cmd_args)
if not has_b:
if args.devices:
device_val = device_arg_from_devices(args.devices)
else:
device_val = "HTP0"
if device_val:
cmd_args += ["-b", device_val]
else:
has_device = any(arg.startswith("--device") for arg in cmd_args)
if not has_device:
if args.devices:
device_val = device_arg_from_devices(args.devices)
else:
device_val = "HTP0"
if device_val:
cmd_args += ["--device", device_val]
# Automatically add -v to known llama tools if sched-debug, verbose, or profile are set
verbose_trigger = (
args.sched_debug
or args.verbose is not None
or args.profile is not None
or args.hex_verbose is not None
or args.hex_profile is not None
or args.hex_optrace is not None
)
if verbose_trigger and basename in ("llama-cli", "llama-completion", "llama-bench", "llama-server", "llama-mtmd-cli"):
if "-v" not in cmd_args and "--verbose" not in cmd_args:
cmd_args.append("-v")
# Inject defaults for llama-cli, llama-completion, and llama-server if not overridden by the user
if basename in ("llama-cli", "llama-completion", "llama-server"):
if "-ngl" not in cmd_args and "--n-gpu-layers" not in cmd_args:
cmd_args += ["-ngl", "99"]
if "-fa" not in cmd_args and "--flash-attn" not in cmd_args:
cmd_args += ["-fa", "on"]
# Use ubatch-size 1024 for hexagon backend (HTP devices)
if hex_devices and basename in ("llama-cli", "llama-completion", "llama-server", "llama-bench"):
if "--ubatch-size" not in cmd_args and "-ub" not in cmd_args:
cmd_args += ["--ubatch-size", "1024"]
elif basename in ("llama-cli", "llama-completion", "llama-server"):
if "--ubatch-size" not in cmd_args and "-ub" not in cmd_args:
cmd_args += ["--ubatch-size", "1024"]
if basename in ("llama-cli", "llama-completion", "llama-server", "llama-bench"):
if "-t" not in cmd_args and "--threads" not in cmd_args:
cmd_args += ["-t", "6"]
if getattr(args, "no_embd_offload", False):
if not any("token_embd" in arg or "output.weight" in arg for arg in cmd_args):
cmd_args += ["-ot", r"^(token_embd|output)\.weight$=CPU"]
# Resolve target directory on device
target_dir = args.target_dir
if not target_dir:
target_dir = default_target_dir(target_type, target_transport, args.home_dir)
if target_transport == "adb":
adb_base = ["adb"]
if target_val: # serial
adb_base += ["-s", target_val]
if target_type == "linux":
adb_root(adb_base)
env_parts = library_env_parts("./lib")
if target_type == "linux":
env_parts.insert(0, f"HOME={args.home_dir or '/home'}")
for k, v in env_vars.items():
env_parts.append(f"{k}={v}")
env_str = " ".join(env_parts)
cmd_str = shlex_join(cmd_args)
adb_shell_cmd = f"cd {target_dir} && ulimit -c unlimited && {env_str} {cmd_str}"
full_cmd = adb_base + ["shell", adb_shell_cmd]
logger.info(f"+ {' '.join(full_cmd)}")
res = subprocess.run(full_cmd)
sys.exit(res.returncode)
elif target_type == "linux":
ssh_host = target_val
if not ssh_host:
logger.error("Error: SSH host not specified in target (e.g. use linux:user@host, lnx:user@host, or ubuntu:user@host). Cannot execute.")
sys.exit(1)
# Linux remote run via SSH
env_parts = library_env_parts("./lib")
for k, v in env_vars.items():
env_parts.append(f"{k}={v}")
env_str = " ".join(env_parts)
cmd_str = shlex_join(cmd_args)
ssh_shell_cmd = f"cd {target_dir} && ulimit -c unlimited && {env_str} {cmd_str}"
full_cmd = ["ssh", ssh_host, ssh_shell_cmd]
logger.info(f"+ {' '.join(full_cmd)}")
res = subprocess.run(full_cmd)
sys.exit(res.returncode)
elif target_type == "windows":
logger.info("Windows target execution is currently a stub.")
sys.exit(0)
else:
# Run locally
local_env = os.environ.copy()
lib_dir = os.path.normpath(os.path.join(package_path, "lib"))
if platform.system() == "Windows":
local_env["PATH"] = lib_dir + os.path.pathsep + local_env.get("PATH", "")
else:
local_env["LD_LIBRARY_PATH"] = lib_dir + os.path.pathsep + local_env.get("LD_LIBRARY_PATH", "")
for k in MANAGED_ENV_NAMES:
local_env.pop(k, None)
for name in DSP_LIBRARY_ENV_NAMES:
local_env[name] = lib_dir
for k, v in env_vars.items():
local_env[k] = v
logger.info(f"+ {shlex_join(cmd_args)}")
res = subprocess.run(cmd_args, env=local_env)
sys.exit(res.returncode)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("\nInterrupted by user.")
sys.exit(130)
+62
View File
@@ -0,0 +1,62 @@
import os
from pathlib import Path
SDK_CONFIGS = (
{
"name": "Hexagon SDK",
"repo": "snapdragon-toolchain/hexagon-sdk",
"default_version": "6.7.0.0",
"parent_dir": "Hexagon_SDK",
"archive_prefix": "hexagon-sdk-v",
"markers": ("hexagon_sdk.json",),
},
{
"name": "OpenCL SDK",
"repo": "snapdragon-toolchain/opencl-sdk",
"default_version": "2.3.2",
"parent_dir": "OpenCL_SDK",
"archive_prefix": "adreno-opencl-sdk-v",
"markers": ("include/CL", "lib/OpenCL.lib"),
},
)
def is_valid_sdk(config, target_dir):
return target_dir.is_dir() and all((target_dir / marker).exists() for marker in config["markers"])
def get_hexagon_tools_dir(hexagon_dir):
tools_parent = hexagon_dir / "tools" / "HEXAGON_Tools"
if not tools_parent.is_dir():
raise RuntimeError(f"Expected Hexagon tools directory in {tools_parent}")
tools_dirs = [path for path in tools_parent.iterdir() if path.is_dir()]
if len(tools_dirs) != 1:
raise RuntimeError(f"Expected one Hexagon tools directory in {tools_parent}")
return tools_dirs[0]
def validate_windows_sdks():
hexagon_config, opencl_config = SDK_CONFIGS
hexagon_dir = os.environ.get("HEXAGON_SDK_ROOT")
tools_dir = os.environ.get("HEXAGON_TOOLS_ROOT")
opencl_dir = os.environ.get("OPENCL_SDK_ROOT")
missing = []
expected_tools_dir = None
if not hexagon_dir or not is_valid_sdk(hexagon_config, Path(hexagon_dir)):
missing.append("HEXAGON_SDK_ROOT")
else:
try:
expected_tools_dir = get_hexagon_tools_dir(Path(hexagon_dir))
except RuntimeError:
pass
if not tools_dir or not expected_tools_dir or Path(tools_dir) != expected_tools_dir:
missing.append("HEXAGON_TOOLS_ROOT")
if not opencl_dir or not is_valid_sdk(opencl_config, Path(opencl_dir)):
missing.append("OPENCL_SDK_ROOT")
if missing:
raise RuntimeError(
f"Missing or invalid Windows SDK paths: {', '.join(missing)}. "
"Run scripts/snapdragon/setup-sdk.py first."
)
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
#
# Install Windows on Snapdragon SDKs for llama.cpp.
#
import sys
import os
import argparse
import shutil
import logging
import json
import hashlib
import tarfile
import tempfile
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from sdk import SDK_CONFIGS, get_hexagon_tools_dir, is_valid_sdk
logger = logging.getLogger("setup_sdk")
DEFAULT_SDK_BASE_DIR = r"C:\Qualcomm"
def get_sdk_releases(config):
request = Request(
f"https://api.github.com/repos/{config['repo']}/releases?per_page=100",
headers={"Accept": "application/vnd.github+json", "User-Agent": "llama.cpp"},
)
try:
with urlopen(request, timeout=30) as response:
releases = json.load(response)
except (HTTPError, URLError, TimeoutError) as err:
raise RuntimeError(f"Cannot query {config['name']} releases: {err}") from err
result = []
for release in releases:
if release["draft"] or release["prerelease"]:
continue
version = release["tag_name"].removeprefix("v")
archive_name = f"{config['archive_prefix']}{version}-arm64-wos.tar.xz"
for asset in release["assets"]:
if asset["name"] != archive_name:
continue
result.append({
"version": version,
"name": asset["name"],
"url": asset["browser_download_url"],
"sha256": (asset.get("digest") or "").removeprefix("sha256:"),
})
return result
def list_sdk_releases():
for config in SDK_CONFIGS:
logger.info("%s:", config["name"])
releases = get_sdk_releases(config)
if not releases:
logger.info(" no Windows on Snapdragon releases found")
continue
for release in releases:
logger.info(" %s: %s", release["version"], release["name"])
def get_sdk_release(config, version):
version = version or config["default_version"]
version = version.removeprefix("v")
for release in get_sdk_releases(config):
if release["version"] == version:
if not release["sha256"]:
raise RuntimeError(f"{config['name']} {version} does not provide a SHA-256 digest")
return release
raise RuntimeError(
f"No Windows on Snapdragon release for {config['name']} {version}. "
"Run scripts/snapdragon/setup-sdk.py --list-sdk-releases to see available versions."
)
def sha256sum(path):
digest = hashlib.sha256()
with open(path, "rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def download_sdk(release, archive):
while True:
if archive.exists() and sha256sum(archive) == release["sha256"]:
logger.info("Using existing archive %s", archive)
return
offset = archive.stat().st_size if archive.exists() else 0
headers = {"User-Agent": "llama.cpp"}
if offset:
headers["Range"] = f"bytes={offset}-"
logger.info("Resuming download of %s at %d MiB", release["name"], offset // (1024 * 1024))
else:
logger.info("Downloading %s", release["name"])
try:
with urlopen(Request(release["url"], headers=headers), timeout=30) as response:
mode = "ab" if offset and response.status == 206 else "wb"
with open(archive, mode) as file:
shutil.copyfileobj(response, file)
except HTTPError as err:
if err.code != 416:
raise RuntimeError(f"Cannot download {release['name']}: {err}") from err
archive.unlink(missing_ok=True)
continue
except (URLError, TimeoutError) as err:
raise RuntimeError(f"Cannot download {release['name']}: {err}") from err
if sha256sum(archive) == release["sha256"]:
return
raise RuntimeError(f"SHA-256 mismatch for {archive}. Re-run the command to resume the download.")
def extract_sdk(config, archive, target_dir):
if not hasattr(tarfile, "data_filter"):
raise RuntimeError("SDK extraction requires Python 3.10.12 or later")
with tempfile.TemporaryDirectory(prefix=f".{target_dir.name}.tmp-", dir=target_dir.parent) as staging_path:
staging_dir = Path(staging_path)
with tarfile.open(archive, "r:xz") as tar:
tar.extractall(staging_dir, filter=tarfile.data_filter)
candidates = [staging_dir] + [path for path in staging_dir.iterdir() if path.is_dir()]
extracted_dirs = [path for path in candidates if is_valid_sdk(config, path)]
if len(extracted_dirs) != 1:
raise RuntimeError(f"{config['name']} archive does not contain the expected files")
extracted_dir = extracted_dirs[0]
backup_dir = None
if target_dir.exists():
backup_dir = target_dir.parent / f".{target_dir.name}.backup"
if backup_dir.exists():
raise RuntimeError(f"Cannot replace {target_dir}: backup directory {backup_dir} already exists")
target_dir.replace(backup_dir)
try:
extracted_dir.replace(target_dir)
except Exception:
if backup_dir:
backup_dir.replace(target_dir)
raise
if backup_dir:
shutil.rmtree(backup_dir)
def install_sdk(config, version, base_dir, force):
version = (version or config["default_version"]).removeprefix("v")
target_dir = base_dir / config["parent_dir"] / version
if is_valid_sdk(config, target_dir) and not force:
logger.info("Using existing %s at %s", config["name"], target_dir)
return target_dir
release = get_sdk_release(config, version)
target_dir.parent.mkdir(parents=True, exist_ok=True)
archive = target_dir.parent / release["name"]
download_sdk(release, archive)
logger.info("Extracting %s to %s", config["name"], target_dir)
extract_sdk(config, archive, target_dir)
archive.unlink(missing_ok=True)
return target_dir
def set_user_environment(values):
if os.name != "nt":
raise RuntimeError("SDK setup must run on Windows")
import winreg
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, "Environment") as key:
for name, value in values.items():
winreg.SetValueEx(key, name, 0, winreg.REG_SZ, str(value))
os.environ[name] = str(value)
import ctypes
result = ctypes.c_ulong()
ctypes.windll.user32.SendMessageTimeoutW(0xffff, 0x001a, 0, "Environment", 0x0002, 5000, ctypes.byref(result))
def setup_sdks(args):
base_dir = Path(args.sdk_base_dir).expanduser().resolve()
hexagon_config, opencl_config = SDK_CONFIGS
environment = {}
if args.hexagon is not None:
hexagon_dir = install_sdk(hexagon_config, args.hexagon, base_dir, args.force)
environment["HEXAGON_SDK_ROOT"] = hexagon_dir
environment["HEXAGON_TOOLS_ROOT"] = get_hexagon_tools_dir(hexagon_dir)
if args.opencl is not None:
opencl_dir = install_sdk(opencl_config, args.opencl, base_dir, args.force)
environment["OPENCL_SDK_ROOT"] = opencl_dir
set_user_environment(environment)
logger.info("SDK environment variables were updated. Start a new terminal before building.")
def main():
logging.basicConfig(level=logging.INFO, format="%(message)s")
parser = argparse.ArgumentParser(description="Install Windows on Snapdragon SDKs for llama.cpp.")
parser.add_argument("--list-sdk-releases", action="store_true", help="List available Windows on Snapdragon SDK releases")
parser.add_argument("--sdk-base-dir", default=DEFAULT_SDK_BASE_DIR, help=r"SDK installation directory (default: C:\Qualcomm)")
parser.add_argument("--hexagon", nargs="?", const=SDK_CONFIGS[0]["default_version"], metavar="VERSION", help="Install the Hexagon SDK, optionally selecting a version")
parser.add_argument("--opencl", nargs="?", const=SDK_CONFIGS[1]["default_version"], metavar="VERSION", help="Install the OpenCL SDK, optionally selecting a version")
parser.add_argument("--force", action="store_true", help="Reinstall selected SDKs even when they already exist")
args = parser.parse_args()
if args.list_sdk_releases:
if args.sdk_base_dir != DEFAULT_SDK_BASE_DIR or args.hexagon is not None or args.opencl is not None or args.force:
parser.error("Installation options cannot be combined with --list-sdk-releases")
list_sdk_releases()
return
if args.hexagon is None and args.opencl is None:
parser.error("Select at least one SDK with --hexagon or --opencl")
if os.name != "nt":
parser.error("SDK setup must run on Windows")
setup_sdks(args)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logger.info("\nInterrupted by user.")
sys.exit(130)
except RuntimeError as err:
logger.error("Error: %s", err)
sys.exit(1)
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
#
# Synchronize ggml changes to llama.cpp
#
# Usage:
#
# $ cd /path/to/llama.cpp
# $ ./scripts/sync-ggml-am.sh -skip hash0,hash1,hash2... -C 3
#
set -e
sd=$(dirname $0)
cd $sd/../
SRC_LLAMA=$(pwd)
SRC_GGML=$(cd ../ggml; pwd)
if [ ! -d $SRC_GGML ]; then
echo "ggml not found at $SRC_GGML"
exit 1
fi
lc=$(cat $SRC_LLAMA/scripts/sync-ggml.last)
echo "Syncing ggml changes since commit $lc"
to_skip=""
# context for git patches in number of lines
ctx="8"
while [ "$1" != "" ]; do
case $1 in
-skip )
shift
to_skip=$1
;;
-C )
shift
ctx=$1
;;
esac
shift
done
cd $SRC_GGML
git log --oneline $lc..HEAD
git log --oneline $lc..HEAD --reverse | grep -v "(llama/[0-9]*)" | cut -d' ' -f1 > $SRC_LLAMA/ggml-commits
if [ ! -s $SRC_LLAMA/ggml-commits ]; then
rm -v $SRC_LLAMA/ggml-commits
echo "No new commits"
exit 0
fi
if [ -f $SRC_LLAMA/ggml-src.patch ]; then
rm -v $SRC_LLAMA/ggml-src.patch
fi
while read c; do
if [ -n "$to_skip" ]; then
if [[ $to_skip == *"$c"* ]]; then
echo "Skipping $c"
continue
fi
fi
git format-patch -U${ctx} -k $c~1..$c --stdout -- \
CMakeLists.txt \
src/CMakeLists.txt \
cmake/BuildTypes.cmake \
cmake/GitVars.cmake \
cmake/common.cmake \
cmake/ggml-config.cmake.in \
src/ggml-cpu/cmake/FindSIMD.cmake \
src/ggml* \
include/ggml*.h \
include/gguf*.h \
tests/test-opt.cpp \
tests/test-quantize-fns.cpp \
tests/test-quantize-perf.cpp \
tests/test-backend-ops.cpp \
LICENSE \
scripts/gen-authors.sh \
>> $SRC_LLAMA/ggml-src.patch
done < $SRC_LLAMA/ggml-commits
rm -v $SRC_LLAMA/ggml-commits
# delete files if empty
if [ ! -s $SRC_LLAMA/ggml-src.patch ]; then
rm -v $SRC_LLAMA/ggml-src.patch
fi
cd $SRC_LLAMA
if [ -f $SRC_LLAMA/ggml-src.patch ]; then
# replace PR numbers
#
# Subject: some text (#1234)
# Subject: some text (ggml/1234)
cat ggml-src.patch | sed -e 's/^Subject: \(.*\) (#\([0-9]*\))/Subject: \1 (ggml\/\2)/' > ggml-src.patch.tmp
mv ggml-src.patch.tmp ggml-src.patch
cat ggml-src.patch | sed -e 's/^\(.*\) (#\([0-9]*\))$/\1 (ggml\/\2)/' > ggml-src.patch.tmp
mv ggml-src.patch.tmp ggml-src.patch
# replace filenames:
#
# CMakelists.txt -> ggml/CMakeLists.txt
# src/CMakeLists.txt -> ggml/src/CMakeLists.txt
# cmake/BuildTypes.cmake -> ggml/cmake/BuildTypes.cmake
# cmake/GitVars.cmake -> ggml/cmake/GitVars.cmake
# cmake/common.cmake -> ggml/cmake/common.cmake
# cmake/ggml-config.cmake.in -> ggml/cmake/ggml-config.cmake.in
# src/ggml-cpu/cmake/FindSIMD.cmake -> ggml/src/ggml-cpu/cmake/FindSIMD.cmake
#
# src/ggml* -> ggml/src/ggml*
#
# include/ggml*.h -> ggml/include/ggml*.h
# include/gguf*.h -> ggml/include/gguf*.h
#
# tests/test*.cpp -> tests/
#
# LICENSE -> LICENSE
# scripts/gen-authors.sh -> scripts/gen-authors.sh
cat ggml-src.patch | sed -E \
-e 's/([[:space:]]| [ab]\/)CMakeLists.txt/\1ggml\/CMakeLists.txt/g' \
-e 's/([[:space:]]| [ab]\/)src\/CMakeLists.txt/\1ggml\/src\/CMakeLists.txt/g' \
-e 's/([[:space:]]| [ab]\/)cmake\/BuildTypes.cmake/\1ggml\/cmake\/BuildTypes.cmake/g' \
-e 's/([[:space:]]| [ab]\/)cmake\/GitVars.cmake/\1ggml\/cmake\/GitVars.cmake/g' \
-e 's/([[:space:]]| [ab]\/)cmake\/common.cmake/\1ggml\/cmake\/common.cmake/g' \
-e 's/([[:space:]]| [ab]\/)cmake\/ggml-config.cmake.in/\1ggml\/cmake\/ggml-config.cmake.in/g' \
-e 's/([[:space:]]| [ab]\/)src\/ggml-cpu\/cmake\/FindSIMD.cmake/\1ggml\/src\/ggml-cpu\/cmake\/FindSIMD.cmake/g' \
-e 's/([[:space:]]| [ab]\/)src\/ggml(.*)/\1ggml\/src\/ggml\2/g' \
-e 's/([[:space:]]| [ab]\/)include\/ggml(.*)\.h/\1ggml\/include\/ggml\2.h/g' \
-e 's/([[:space:]]| [ab]\/)include\/gguf(.*)\.h/\1ggml\/include\/gguf\2.h/g' \
-e 's/([[:space:]]| [ab]\/)tests\/(.*)\.cpp/\1tests\/\2.cpp/g' \
-e 's/([[:space:]]| [ab]\/)LICENSE/\1LICENSE/g' \
-e 's/([[:space:]]| [ab]\/)scripts\/gen-authors\.sh/\1scripts\/gen-authors.sh/g' \
> ggml-src.patch.tmp
mv ggml-src.patch.tmp ggml-src.patch
git am -C${ctx} ggml-src.patch
rm -v $SRC_LLAMA/ggml-src.patch
fi
# update last commit
cd $SRC_GGML
git log -1 --format=%H > $SRC_LLAMA/scripts/sync-ggml.last
echo "Done"
exit 0
+1
View File
@@ -0,0 +1 @@
f1aa6a3465ffad40dadd390ac16b9deedd4e97f7
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
cp -rpv ../ggml/CMakeLists.txt ./ggml/CMakeLists.txt
cp -rpv ../ggml/src/CMakeLists.txt ./ggml/src/CMakeLists.txt
cp -rpv ../ggml/cmake/* ./ggml/cmake/
cp -rpv ../ggml/src/ggml-cpu/cmake/* ./ggml/src/ggml-cpu/cmake/
cp -rpv ../ggml/src/ggml* ./ggml/src/
cp -rpv ../ggml/include/ggml*.h ./ggml/include/
cp -rpv ../ggml/include/gguf*.h ./ggml/include/
cp -rpv ../ggml/tests/test-opt.cpp ./tests/test-opt.cpp
cp -rpv ../ggml/tests/test-quantize-fns.cpp ./tests/test-quantize-fns.cpp
cp -rpv ../ggml/tests/test-quantize-perf.cpp ./tests/test-quantize-perf.cpp
cp -rpv ../ggml/tests/test-backend-ops.cpp ./tests/test-backend-ops.cpp
cp -rpv ../LICENSE ./LICENSE
cp -rpv ../ggml/scripts/gen-authors.sh ./scripts/gen-authors.sh
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
import urllib.request
import os
import sys
import subprocess
HTTPLIB_VERSION = "refs/tags/v0.62.0"
# used by examples/gguf-hash, these repos have no release tag, so we pin a commit
XXHASH_COMMIT = "9f465f1ea932d6ad9a26cd77496311ffa544cd68"
SHA1_COMMIT = "e1e2536fcf6a8f9703be8c85d58724b408552287"
SHA256_COMMIT = "5e637272c13f200872d55ff579f7e2ab6c3f252f"
ROTATE_BITS_COMMIT = "27e784942f67db44abf2115c6638e735b579acd1"
vendor = {
"https://github.com/nlohmann/json/releases/latest/download/json.hpp": "vendor/nlohmann/json.hpp",
"https://github.com/nlohmann/json/releases/latest/download/json_fwd.hpp": "vendor/nlohmann/json_fwd.hpp",
"https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_image.h": "vendor/stb/stb_image.h",
# not using latest tag to avoid this issue: https://github.com/ggml-org/llama.cpp/pull/17179#discussion_r2515877926
# "https://github.com/mackron/miniaudio/raw/refs/tags/0.11.24/miniaudio.h": "vendor/miniaudio/miniaudio.h",
"https://github.com/mackron/miniaudio/raw/9634bedb5b5a2ca38c1ee7108a9358a4e233f14d/miniaudio.h": "vendor/miniaudio/miniaudio.h",
f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/httplib.h": "httplib.h",
f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/split.py": "split.py",
f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/LICENSE": "vendor/cpp-httplib/LICENSE",
"https://raw.githubusercontent.com/sheredom/subprocess.h/0dccaa9aa176dd6d7ef8afeca3c18d6e80a32795/subprocess.h": "vendor/sheredom/subprocess.h",
f"https://raw.githubusercontent.com/Cyan4973/xxHash/{XXHASH_COMMIT}/xxhash.c": "vendor/hash/xxhash/xxhash.c",
f"https://raw.githubusercontent.com/Cyan4973/xxHash/{XXHASH_COMMIT}/xxhash.h": "vendor/hash/xxhash/xxhash.h",
f"https://raw.githubusercontent.com/Cyan4973/xxHash/{XXHASH_COMMIT}/LICENSE": "vendor/hash/xxhash/LICENSE",
# clibs/sha1 ships no license file, the source header says public domain
f"https://raw.githubusercontent.com/clibs/sha1/{SHA1_COMMIT}/sha1.c": "vendor/hash/sha1/sha1.c",
f"https://raw.githubusercontent.com/clibs/sha1/{SHA1_COMMIT}/sha1.h": "vendor/hash/sha1/sha1.h",
f"https://raw.githubusercontent.com/jb55/sha256.c/{SHA256_COMMIT}/sha256.c": "vendor/hash/sha256/sha256.c",
f"https://raw.githubusercontent.com/jb55/sha256.c/{SHA256_COMMIT}/sha256.h": "vendor/hash/sha256/sha256.h",
f"https://raw.githubusercontent.com/jb55/sha256.c/{SHA256_COMMIT}/LICENSE": "vendor/hash/sha256/LICENSE",
f"https://raw.githubusercontent.com/jb55/rotate-bits.h/{ROTATE_BITS_COMMIT}/rotate-bits.h": "vendor/hash/rotate-bits/rotate-bits.h",
f"https://raw.githubusercontent.com/jb55/rotate-bits.h/{ROTATE_BITS_COMMIT}/LICENSE.md": "vendor/hash/rotate-bits/LICENSE.md",
}
# local changes kept on top of the upstream sources
patches = {
"vendor/hash/xxhash/xxhash.h": [(
'#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* >= C11 */\n',
'/* Windows SDK under 10.0.22000 is missing stdalign.h so we add a check\n'
' before allowing the windows compiler to use the C11 form.\n'
' Reference: https://github.com/Cyan4973/xxHash/issues/955 */\n'
'#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) \\\n'
' && (defined(_MSC_VER) && (_MSC_VER >= 1000) || !defined(_MSC_VER)) /* >= C11 */\n'
)],
# sha1 exports a bare "SHA1" symbol, which clashes with the boringssl one at link time.
# we compile it as C++ (see vendor/hash/CMakeLists.txt) and put it in a namespace.
"vendor/hash/sha1/sha1.h": [
(
'#if defined(__cplusplus)\n'
'extern "C" {\n'
'#endif\n',
'namespace vendor_hash {\n'
),
(
'#if defined(__cplusplus)\n'
'}\n'
'#endif\n',
'} // namespace vendor_hash\n'
),
],
"vendor/hash/sha1/sha1.c": [
(
'#include "sha1.h"\n',
'#include "sha1.h"\n'
'\n'
'namespace vendor_hash {\n'
),
(
' SHA1Final((unsigned char *)hash_out, &ctx);\n'
'}\n',
' SHA1Final((unsigned char *)hash_out, &ctx);\n'
'}\n'
'\n'
'} // namespace vendor_hash\n'
),
],
# silence a maybe-uninitialized warning
"vendor/hash/sha256/sha256.c": [(
" uint32_t W[16];\n",
" uint32_t W[16] = {0};\n"
)],
}
# local changes too large for the replacements above, kept as diffs and applied with git apply
patch_files = [
# backport of the fix for the stack overflow on deeply nested values (nlohmann/json#5387)
# TODO: remove once nlohmann/json releases a version newer than 3.12.0
"vendor/nlohmann/json-deep-nesting.patch",
]
for url, filename in vendor.items():
print(f"downloading {url} to {filename}") # noqa: NP100
urllib.request.urlretrieve(url, filename)
for filename, replacements in patches.items():
print(f"patching {filename}") # noqa: NP100
with open(filename, "r", encoding="utf-8", newline="") as f:
content = f.read()
for old, new in replacements:
if content.count(old) != 1:
print(f"Error: cannot apply patch on {filename}, upstream code has changed") # noqa: NP100
sys.exit(1)
content = content.replace(old, new)
with open(filename, "w", encoding="utf-8", newline="") as f:
f.write(content)
for patch_file in patch_files:
print(f"applying {patch_file}") # noqa: NP100
try:
subprocess.check_call(["git", "apply", patch_file])
except subprocess.CalledProcessError:
print(f"Error: cannot apply {patch_file}, upstream code has changed") # noqa: NP100
sys.exit(1)
print("Splitting httplib.h...") # noqa: NP100
try:
subprocess.check_call([
sys.executable, "split.py",
"--extension", "cpp",
"--out", "vendor/cpp-httplib"
])
except Exception as e:
print(f"Error: {e}") # noqa: NP100
sys.exit(1)
finally:
os.remove("split.py")
os.remove("httplib.h")
+379
View File
@@ -0,0 +1,379 @@
#!/usr/bin/env uv run
'''
Simplistic tool call benchmarks for llama-server and ollama.
Essentially runs the tests at server/tools/server/tests/unit/test_tool_call.py N times, at different temperatures and on different backends (current llama-server, baseline llama-server and ollama),
and plots the results of multiple runs (from same .jsonl file or multiple ones) as a success rate heatmap.
Simple usage example:
cmake -B build && cmake --build build --config Release -j -t llama-server
export LLAMA_SERVER_BIN_PATH=$PWD/build/bin/llama-server
export LLAMA_CACHE=${LLAMA_CACHE:-$HOME/Library/Caches/llama.cpp}
./scripts/tool_bench.py run --n 10 --temp -1 --temp 0 --temp 1 --temp 2 --temp 5 --llama-baseline $PWD/buildMaster/bin/llama-server --output qwen14b.jsonl --hf bartowski/Qwen2.5-14B-Instruct-GGUF:Q4_K_L
./scripts/tool_bench.py run --n 30 --temp -1 --temp 0 --temp 1 --model "Qwen 2.5 1.5B Q4_K_M" --output qwen1.5b.jsonl --hf bartowski/Qwen2.5-1.5B-Instruct-GGUF --ollama qwen2.5:1.5b-instruct-q4_K_M
./scripts/tool_bench.py run --n 30 --temp -1 --temp 0 --temp 1 --model "Qwen 2.5 Coder 7B Q4_K_M" --output qwenc7b.jsonl --hf bartowski/Qwen2.5-Coder-7B-Instruct-GGUF --ollama qwen2.5-coder:7b
./scripts/tool_bench.py plot *.jsonl # Opens window w/ heatmap
./scripts/tool_bench.py plot qwen*.jsonl --output qwen.png # Saves heatmap to qwen.png
(please see ./scripts/tool_bench.sh for a more complete example)
'''
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pytest",
# "pandas",
# "matplotlib",
# "seaborn",
# "requests",
# "wget",
# "typer",
# ]
# ///
from contextlib import contextmanager
from pathlib import Path
import re
from statistics import mean, median
from typing import Annotated, Dict, List, Optional, Tuple
import atexit
import json
import logging
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import subprocess
import sys
import time
import typer
sys.path.insert(0, Path(__file__).parent.parent.as_posix())
if True:
from tools.server.tests.utils import ServerProcess # ty: ignore[unresolved-import]
from tools.server.tests.unit.test_tool_call import do_test_calc_result, do_test_hello_world, do_test_weather # ty: ignore[unresolved-import]
@contextmanager
def scoped_server(sp: ServerProcess):
def stop():
nonlocal sp
if sp is not None:
sp.stop()
sp = None # type: ignore
atexit.register(stop)
yield sp
stop()
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = typer.Typer()
@app.command()
def plot(files: List[Path], output: Optional[Path] = None, test_regex: Optional[str] = None, server_regex: Optional[str] = None):
lines: List[Dict] = []
for file in files:
if not file.exists():
logger.error(f"File not found: {file}")
continue
try:
with file.open() as f:
raw_data = f.read()
logger.info(f"Reading {file} ({len(raw_data)} bytes)")
for line_num, line in enumerate(raw_data.split('\n'), 1):
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
lines.append(record)
except json.JSONDecodeError as e:
logger.warning(f"Invalid JSON at {file}:{line_num} - {e}")
except Exception as e:
logger.error(f"Error processing {file}: {e}")
if not lines:
raise Exception("No valid data was loaded")
data_dict: Dict[Tuple, float] = {}
models: List[str] = []
temps = set()
tests = set()
server_names = set()
total_counts = set()
for rec in lines:
try:
model = rec["model"]
temp = rec["temp"]
server_name = rec["server_name"]
test = rec["test"]
success = rec["success_ratio"]
success_count = rec["success_count"]
failure_count = rec["failure_count"]
total_count = success_count + failure_count
total_counts.add(total_count)
if test_regex and not re.search(test_regex, test):
continue
if server_regex and not re.search(server_regex, server_name):
continue
data_dict[(model, temp, server_name, test)] = success
if model not in models:
models.append(model)
temps.add(temp)
tests.add(test)
server_names.add(server_name)
except KeyError as e:
logger.warning(f"Missing required field in record: {e}")
if len(total_counts) > 1:
logger.warning(f"Total counts are not consistent: {total_counts}")
# Sort the collected values
temps = list(sorted(temps, key=lambda x: x if x is not None else -1))
tests = list(sorted(tests))
server_names = list(sorted(server_names))
logger.info(f"Processed {len(lines)} lines")
logger.info(f"Found {len(data_dict)} valid data points")
logger.info(f"Models: {models}")
logger.info(f"Temperatures: {temps}")
logger.info(f"Tests: {tests}")
logger.info(f"Servers: {server_names}")
matrix: list[list[float]] = []
index: list[str] = []
all_cols = [
(server_name, test)
for server_name in server_names
for test in tests
]
for model in models:
for temp in temps:
index.append(f"{model} @ {temp}")
row_vals = [
data_dict.get((model, temp, server_name, test), np.nan)
for server_name, test in all_cols
]
matrix.append(row_vals)
columns: list[str] = [f"{server_name}\n{test}" for server_name, test in all_cols]
df = pd.DataFrame(matrix, index=np.array(index), columns=np.array(columns))
plt.figure(figsize=(12, 6))
sns.heatmap(
df, annot=True, cmap="RdYlGn", vmin=0.0, vmax=1.0, cbar=True, fmt=".2f", center=0.5, square=True, linewidths=0.5,
cbar_kws={"label": "Success Ratio"},
)
plt.title(f"Tool Call Bench (n = {str(min(total_counts)) if len(total_counts) == 1 else f'{min(total_counts)}-{max(total_counts)}'})\nSuccess Ratios by Server & Test", pad=20)
plt.xlabel("Server & Test", labelpad=10)
plt.ylabel("Model @ Temperature", labelpad=10)
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
if output:
plt.savefig(output, dpi=300, bbox_inches='tight')
logger.info(f"Plot saved to {output}")
else:
plt.show()
@app.command()
def run(
output: Annotated[Path, typer.Option(help="Output JSON file")],
model: Annotated[Optional[str], typer.Option(help="Name of the model to test (server agnostic)")] = None,
hf: Annotated[Optional[str], typer.Option(help="GGUF huggingface model repo id (+ optional quant) to test w/ llama-server")] = None,
chat_template: Annotated[Optional[str], typer.Option(help="Chat template override for llama-server")] = None,
chat_template_file: Annotated[Optional[str], typer.Option(help="Chat template file override for llama-server")] = None,
ollama: Annotated[Optional[str], typer.Option(help="Ollama model tag to test")] = None,
llama_baseline: Annotated[Optional[str], typer.Option(help="llama-server baseline binary path to use as baseline")] = None,
n: Annotated[int, typer.Option(help="Number of times to run each test")] = 10,
temp: Annotated[Optional[List[float]], typer.Option(help="Set of temperatures to test")] = None,
top_p: Annotated[Optional[float], typer.Option(help="top_p")] = None,
top_k: Annotated[Optional[int], typer.Option(help="top_k")] = None,
ctk: Annotated[Optional[str], typer.Option(help="ctk")] = None,
ctv: Annotated[Optional[str], typer.Option(help="ctv")] = None,
fa: Annotated[Optional[bool], typer.Option(help="fa")] = None,
seed: Annotated[Optional[int], typer.Option(help="Random seed")] = None,
port: Annotated[int, typer.Option(help="llama-server port")] = 8084,
force: Annotated[bool, typer.Option(help="Force overwrite of output file")] = False,
append: Annotated[bool, typer.Option(help="Append to output file")] = False,
test_hello_world: Annotated[bool, typer.Option(help="Whether to run the hello world test")] = True,
test_weather: Annotated[bool, typer.Option(help="Whether to run the weather test")] = True,
test_calc_result: Annotated[bool, typer.Option(help="Whether to run the calc result test")] = False,
):
# Check only one of output and append
n_predict = 512 # High because of DeepSeek R1
# n_ctx = 8192
n_ctx = 2048
if model is None:
if hf is not None:
model = hf.split("/")[-1]
elif ollama is not None:
model = ollama
assert force or append or not output.exists(), f"Output file already exists: {output}; use --force to overwrite"
with output.open('a' if append else 'w') as output_file:
def run(server: ServerProcess, *, server_name: str, model_id: str, temp: Optional[float] = None, output_kwargs={}, request_kwargs={}):
request_kwargs = {**request_kwargs}
if temp is not None:
request_kwargs['temperature'] = temp
if top_p is not None:
request_kwargs['top_p'] = top_p
if top_k is not None:
request_kwargs['top_k'] = top_k
if seed is not None:
request_kwargs['seed'] = seed
request_kwargs['cache_prompt'] = False
tests = {}
if test_hello_world:
tests["hello world"] = lambda server: do_test_hello_world(server, **request_kwargs)
if test_weather:
tests["weather"] = lambda server: do_test_weather(server, **request_kwargs)
if test_calc_result:
tests["calc result"] = lambda server: do_test_calc_result(server, None, 512, **request_kwargs)
for test_name, test in tests.items():
success_count = 0
failure_count = 0
failures = []
success_times = []
failure_times = []
logger.info(f"Running {test_name} ({server_name}, {model}): ")
for i in range(n):
start_time = time.time()
def elapsed():
return time.time() - start_time
try:
test(server)
success_times.append(elapsed())
success_count += 1
logger.info('success')
except Exception as e:
logger.error(f'failure: {e}')
failure_count += 1
failure_times.append(elapsed())
failures.append(str(e))
# import traceback
# traceback.print_exc()
output_file.write(json.dumps({**output_kwargs, **dict(
model=model,
server_name=server_name,
model_id=model_id,
test=test_name,
temp=t,
top_p=top_p,
top_k=top_k,
ctk=ctk,
ctv=ctv,
seed=seed,
success_ratio=float(success_count) / n,
avg_time=mean(success_times + failure_times),
median_time=median(success_times + failure_times),
success_count=success_count,
success_times=success_times,
failure_count=failure_count,
failure_times=failure_times,
failures=list(set(failures)),
)}) + '\n')
output_file.flush()
for t in [None] if temp is None else [t if t >= 0 else None for t in temp]:
if hf is not None:
servers: list[Tuple[str, Optional[str]]] = [('llama-server', None)]
if llama_baseline is not None:
servers.append(('llama-server (baseline)', llama_baseline))
for server_name, server_path in servers:
server = ServerProcess()
server.n_ctx = n_ctx
server.n_slots = 1
server.jinja = True
server.ctk = ctk
server.ctv = ctv
server.fa = "on" if fa else "off"
server.n_predict = n_predict
server.model_hf_repo = hf
server.model_hf_file = None
server.chat_template = chat_template
server.chat_template_file = chat_template_file
server.server_path = server_path
if port is not None:
server.server_port = port
# server.debug = True
with scoped_server(server):
server.start(timeout_seconds=15 * 60)
for ignore_chat_grammar in [False]:
run(
server,
server_name=server_name,
model_id=hf,
temp=t,
output_kwargs=dict(
chat_template=chat_template,
chat_template_file=chat_template_file,
),
request_kwargs=dict(
ignore_chat_grammar=ignore_chat_grammar,
),
)
if ollama is not None:
server = ServerProcess()
server.server_port = 11434
server.server_host = "localhost"
subprocess.check_call(["ollama", "pull", ollama])
with scoped_server(server):
run(
server,
server_name="ollama",
model_id=ollama,
temp=t,
output_kwargs=dict(
chat_template=None,
chat_template_file=None,
),
request_kwargs=dict(
model=ollama,
max_tokens=n_predict,
num_ctx = n_ctx,
),
)
if __name__ == "__main__":
app()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
cmake --build build -j
export LLAMA_CACHE=${LLAMA_CACHE:-$HOME/Library/Caches/llama.cpp}
export LLAMA_SERVER_BIN_PATH=$PWD/build/bin/llama-server
if [ ! -x "$LLAMA_SERVER_BIN_PATH" ]; then
echo "Could not find llama-server binary at $LLAMA_SERVER_BIN_PATH"
exit 1
fi
if [ ! -d "$LLAMA_CACHE" ]; then
echo "Could not find llama cache at $LLAMA_CACHE, please set LLAMA_CACHE explicitly."
exit 1
fi
export ARGS=(
--llama-baseline="$(which llama-server)"
--n 30
--temp -1 # Leaves temperature parameter unset (use the server's default, e.g. 0.6 for ollama)
--temp 0
--temp 0.5
--temp 0.75
--temp 1
--temp 1.5
--temp 2
--temp 5
"$@"
)
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 0.5B Q4_K_M" --output ../qwenc0.5b.jsonl --hf bartowski/Qwen2.5-Coder-0.5B-Instruct-GGUF:Q4_K_M --ollama qwen2.5-coder:0.5b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 1.5B Q4_K_M" --output ../qwenc1.5b.jsonl --hf bartowski/Qwen2.5-Coder-1.5B-Instruct-GGUF:Q4_K_M --ollama qwen2.5-coder:1.5b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 3B Q4_K_M" --output ../qwenc3b.jsonl --hf bartowski/Qwen2.5-Coder-3B-Instruct-GGUF:Q4_K_M --ollama qwen2.5-coder:3b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 7B Q4_K_M" --output ../qwenc7b.jsonl --hf bartowski/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M --ollama qwen2.5-coder:7b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 Coder 32B Q4_K_M" --output ../qwenc32b.jsonl --hf bartowski/Qwen2.5-Coder-32B-Instruct-GGUF:Q4_K_M --ollama qwen2.5-coder:32B-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 1.5B Q4_K_M" --output ../qwen1.5b.jsonl --hf bartowski/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M --ollama qwen2.5:1.5b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 3B Q4_K_M" --output ../qwen3b.jsonl --hf bartowski/Qwen2.5-3B-Instruct-GGUF:Q4_K_M --ollama qwen2.5:3b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Qwen 2.5 7B Q4_K_M" --output ../qwen7b.jsonl --hf bartowski/Qwen2.5-7B-Instruct-GGUF:Q4_K_M --ollama qwen2.5:7b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Llama 3.2 Instruct 1B Q4_K_M" --output ../llama1b.jsonl --hf bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M --ollama llama3.2:1b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Llama 3.2 Instruct 3B Q4_K_M" --output ../llama3b.jsonl --hf bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M --ollama llama3.2:3b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Llama 3.1 Instruct 8B Q4_K_M" --output ../llama8b.jsonl --hf bartowski/Meta-Llama-3.1-8B-Instruct-GGUF:Q4_K_M --ollama llama3.1:8b-instruct-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Llama 3.3 70B Q4_K_M" --output ../llama70b.jsonl --hf bartowski/Llama-3.3-70B-Instruct-GGUF:Q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Mistral Nemo Q4_K_M" --output ../nemo.jsonl --hf bartowski/Mistral-Nemo-Instruct-2407-GGUF:Q4_K_M --ollama mistral-nemo:12b-instruct-2407-q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "Hermes 3 Llama 3.1 8B Q4_K_M" --output ../hermes3.jsonl --hf bartowski/Hermes-3-Llama-3.1-8B-GGUF:Q4_K_M --ollama hermes3:8b-llama3.1-q4_K_M --chat-template-file <( python scripts/get_chat_template.py NousResearch/Hermes-3-Llama-3.1-8B tool_use )
./scripts/tool_bench.py run ${ARGS[@]} --model "Hermes 2 Pro Llama 3 8B Q4_K_M" --output ../hermes2.jsonl --hf bartowski/Hermes-2-Pro-Llama-3-8B-GGUF:Q4_K_M --ollama hermes2:8b-llama3-q4_K_M --chat-template-file <( python scripts/get_chat_template.py NousResearch/Hermes-2-Pro-Llama-3-8B tool_use )
./scripts/tool_bench.py run ${ARGS[@]} --model "Functionary Small V3.2 Q4_K_M" --output ../funct3.2.jsonl --hf bartowski/functionary-small-v3.2-GGUF:Q4_K_M
./scripts/tool_bench.py run ${ARGS[@]} --model "FireFunction V2 IQ1_M" --output ../firef2.jsonl --hf bartowski/firefunction-v2-GGUF:IQ1_M --chat-template-file <( python scripts/get_chat_template.py fireworks-ai/llama-3-firefunction-v2 tool_use )
./scripts/tool_bench.py run ${ARGS[@]} --model "Command R7B 12-2024 Q6_K_L" --output ../c4ai.jsonl --hf bartowski/c4ai-command-r7b-12-2024-GGUF:Q6_K_L --chat-template-file <( python scripts/get_chat_template.py CohereForAI/c4ai-command-r7b-12-2024 tool_use )
./scripts/tool_bench.py run ${ARGS[@]} --model "Gemma 2 2B Q8_0" --output ../gemma2.jsonl --hf bartowski/gemma-2-2b-it-GGUF:Q8_0
./scripts/tool_bench.py run ${ARGS[@]} --model "Phi 4 Instruct Q4_K_M" --output ../phi4.jsonl --hf bartowski/phi-4-GGUF:Q4_K_M # --ollama phi4
./scripts/tool_bench.py run ${ARGS[@]} --model "Phi 3.5 Mini Instruct Q4_K_M" --output ../phi3.5.jsonl --hf bartowski/Phi-3.5-mini-instruct-GGUF:Q4_K_M # --ollama phi3.5:3.8b-mini-instruct-q4_K_M
# ./scripts/tool_bench.py run ${ARGS[@]} --model "DeepSeek R1 Distill Qwen 7B Q6_K_L" --output ../dsqw7.jsonl --hf bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q6_K_L --chat-template-file <( python scripts/get_chat_template.py NousResearch/DeepSeek-R1-Distill-Qwen-7B tool_use )
# ./scripts/tool_bench.py run ${ARGS[@]} --model "DeepSeek R1 Distill Qwen 32B Q4_K_M" --output ../dsqw32.jsonl --hf bartowski/DeepSeek-R1-Distill-Qwen-32B-GGUF:Q4_K_M --chat-template-file <( python scripts/get_chat_template.py NousResearch/DeepSeek-R1-Distill-Qwen-32B tool_use )
for f in ../*.jsonl; do
./scripts/tool_bench.py plot "$f" --output ${f%.jsonl}.png || true
done
+592
View File
@@ -0,0 +1,592 @@
# Provision UI assets and generate ui.cpp/ui.h.
#
# Asset provisioning priority:
# 1. Pre-built assets in SRC_DIST_DIR (manually built by user)
# 2. If BUILD_UI=ON: npm build
# 3. If above did not produce assets and HF_ENABLED=ON: HF Bucket download
# of dist.tar.gz (verified against dist.tar.gz.sha256)
cmake_minimum_required(VERSION 3.18)
set(UI_SOURCE_DIR "" CACHE STRING "UI source directory (to run npm build)")
set(UI_BINARY_DIR "" CACHE STRING "UI binary directory (to store generated files)")
set(LLAMA_SOURCE_DIR "" CACHE STRING "Project source root (to resolve version from git)")
set(HF_BUCKET "" CACHE STRING "Hugging Face bucket name")
set(HF_VERSION "" CACHE STRING "Version to download (empty = resolve from git)")
set(HF_ENABLED "" CACHE STRING "Whether to allow HF Bucket download (ON/OFF)")
set(BUILD_UI "" CACHE STRING "Build UI via npm (ON/OFF)")
set(LLAMA_UI_GZIP "" CACHE STRING "Apply gzip compress to assets to save bandwidth")
set(DIST_DIR "${UI_BINARY_DIR}/dist")
set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")
set(WORK_DIR "${UI_BINARY_DIR}/ui-src")
set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")
set(EMBED_STAMP "${UI_BINARY_DIR}/.ui-embed.sha256")
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
set(UI_H "${UI_BINARY_DIR}/ui.h")
function(mime_from_ext name out_var)
string(FIND "${name}" "." ext REVERSE)
if(ext GREATER -1)
string(SUBSTRING "${name}" ${ext} -1 ext_full)
string(SUBSTRING "${ext_full}" 1 -1 ext_str)
else()
set(ext_str "")
endif()
if(ext_str STREQUAL "html")
set(m "text/html; charset=utf-8")
elseif(ext_str STREQUAL "css")
set(m "text/css")
elseif(ext_str STREQUAL "js")
set(m "application/javascript")
elseif(ext_str STREQUAL "json")
set(m "application/json")
elseif(ext_str STREQUAL "webmanifest")
set(m "application/manifest+json")
elseif(ext_str STREQUAL "svg")
set(m "image/svg+xml")
elseif(ext_str STREQUAL "png")
set(m "image/png")
elseif(ext_str STREQUAL "jpg" OR ext_str STREQUAL "jpeg")
set(m "image/jpeg")
elseif(ext_str STREQUAL "ico")
set(m "image/x-icon")
elseif(ext_str STREQUAL "woff")
set(m "font/woff")
elseif(ext_str STREQUAL "woff2")
set(m "font/woff2")
else()
set(m "application/octet-stream")
endif()
set(${out_var} "${m}" PARENT_SCOPE)
endfunction()
# Fail when a dist tree is present but is missing files the UI needs at
# runtime; catches truncated/stale asset trees early with a useful message.
function(ui_validate_assets files in_dir)
list(LENGTH files n_assets)
if(n_assets EQUAL 0)
return()
endif()
set(found_index FALSE)
set(found_manifest FALSE)
set(found_sw FALSE)
set(found_build_json FALSE)
set(found_version_json FALSE)
set(found_bundle_js FALSE)
set(found_bundle_css FALSE)
set(found_workbox_js FALSE)
foreach(f ${files})
get_filename_component(base "${f}" NAME)
if(base STREQUAL "index.html")
set(found_index TRUE)
elseif(base STREQUAL "manifest.webmanifest")
set(found_manifest TRUE)
elseif(base STREQUAL "sw.js")
set(found_sw TRUE)
elseif(base STREQUAL "build.json")
set(found_build_json TRUE)
elseif(base STREQUAL "version.json")
set(found_version_json TRUE)
elseif(base MATCHES "^bundle.*\\.js$")
set(found_bundle_js TRUE)
elseif(base MATCHES "^bundle.*\\.css$")
set(found_bundle_css TRUE)
elseif(base MATCHES "^workbox.*\\.js$")
set(found_workbox_js TRUE)
endif()
endforeach()
set(missing "")
if(NOT found_index)
list(APPEND missing "index.html")
endif()
if(NOT found_manifest)
list(APPEND missing "manifest.webmanifest")
endif()
if(NOT found_sw)
list(APPEND missing "sw.js")
endif()
if(NOT found_build_json)
list(APPEND missing "build.json")
endif()
if(NOT found_version_json)
list(APPEND missing "version.json")
endif()
if(NOT found_bundle_js)
list(APPEND missing "bundle[hash].js")
endif()
if(NOT found_bundle_css)
list(APPEND missing "bundle[hash].css")
endif()
if(NOT found_workbox_js)
list(APPEND missing "workbox[hash].js")
endif()
if(missing)
set(listing "")
foreach(f ${files})
string(APPEND listing " ${f}\n")
endforeach()
set(missing_list "")
foreach(m ${missing})
string(APPEND missing_list " ${m}\n")
endforeach()
message(FATAL_ERROR
"UI: current asset files:\n${listing}"
"UI: missing required asset(s):\n${missing_list}"
"UI: hint: try cleaning your build directory: ${in_dir}")
endif()
endfunction()
# Generate ui.cpp/ui.h embedding every file of ${dist_dir} (empty table when
# it has no index.html), gzip-compressed when LLAMA_UI_GZIP is enabled.
function(emit_files dist_dir)
set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui")
# Collect the asset list once and reuse it for the fingerprint,
# validation, compression and embedding.
set(assets "")
if(EXISTS "${dist_dir}/index.html")
file(GLOB_RECURSE assets
LIST_DIRECTORIES false
RELATIVE "${dist_dir}"
"${dist_dir}/*")
list(FILTER assets EXCLUDE REGEX "^_gzip/")
list(SORT assets)
endif()
if(LLAMA_UI_GZIP AND NOT DEFINED ENV{SOURCE_DATE_EPOCH})
# Zero the gzip header timestamp so identical inputs give identical
# bytes (and therefore stable ETags) on every machine.
set(ENV{SOURCE_DATE_EPOCH} 0)
endif()
# Fingerprint of every input that determines ui.cpp/ui.h: compression
# settings, the asset tree (names + SHA-256) and this script + templates.
set(fp "${LLAMA_UI_GZIP}|$ENV{SOURCE_DATE_EPOCH}|${CMAKE_VERSION}\n")
foreach(f ${assets})
file(SHA256 "${dist_dir}/${f}" h)
string(APPEND fp "${f} ${h}\n")
endforeach()
foreach(g
"${CMAKE_CURRENT_FUNCTION_LIST_FILE}"
"${UI_TEMPLATE_DIR}/ui.h.in"
"${UI_TEMPLATE_DIR}/ui.cpp.in")
file(SHA256 "${g}" h)
string(APPEND fp "gen ${h}\n")
endforeach()
string(SHA256 fingerprint "${fp}")
if(EXISTS "${EMBED_STAMP}" AND EXISTS "${UI_CPP}" AND EXISTS "${UI_H}")
file(READ "${EMBED_STAMP}" fp_saved)
string(STRIP "${fp_saved}" fp_saved)
if(fp_saved STREQUAL "${fingerprint}")
message(STATUS "UI: assets unchanged, skipping embedding")
return()
endif()
endif()
# Drop the old stamp up front so a crash mid-generation cannot leave
# outputs and stamp out of sync.
file(REMOVE "${EMBED_STAMP}")
ui_validate_assets("${assets}" "${dist_dir}")
set(embed_dir "${dist_dir}")
set(use_gzip FALSE)
if(EXISTS "${dist_dir}/index.html")
if(EXISTS "${dist_dir}/_gzip")
# a _gzip tree inside dist_dir can only be a leftover from an
# older version of this script that staged it there
file(REMOVE_RECURSE "${dist_dir}/_gzip")
message(STATUS "UI: removed stale gzip tree ${dist_dir}/_gzip")
endif()
if(LLAMA_UI_GZIP)
# Compress every asset into a parallel _gzip/ tree under the build
# directory, served with Content-Encoding: gzip.
set(gzip_root "${UI_BINARY_DIR}/ui-gzip")
set(gzip_dir "${gzip_root}/_gzip")
file(REMOVE_RECURSE "${gzip_root}")
foreach(f IN LISTS assets)
get_filename_component(asset_path "${dist_dir}/${f}" REALPATH)
get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY)
file(MAKE_DIRECTORY "${dst_dir}")
file(ARCHIVE_CREATE
OUTPUT "${gzip_dir}/${f}"
PATHS "${asset_path}"
FORMAT raw
COMPRESSION GZip
)
endforeach()
message(STATUS "UI: gzip compression applied (${gzip_dir})")
set(embed_dir "${gzip_dir}")
set(use_gzip TRUE)
endif()
endif()
list(LENGTH assets n_assets)
# Per-asset arrays and table rows go into the ui.h.in / ui.cpp.in templates;
# configure_file only rewrites on content change, avoiding needless recompiles.
set(ASSET_ARRAYS "")
set(ASSET_TABLE "")
set(idx 0)
foreach(f IN LISTS assets)
file(READ "${embed_dir}/${f}" hex HEX)
if(hex STREQUAL "")
message(FATAL_ERROR "UI: empty file: ${embed_dir}/${f}")
endif()
string(REGEX REPLACE "(..)" "0x\\1," bytes "${hex}")
file(SHA256 "${embed_dir}/${f}" etag)
mime_from_ext("${f}" mime)
string(APPEND ASSET_ARRAYS
"static const unsigned char asset_${idx}[] = {${bytes}};\n")
string(APPEND ASSET_TABLE
" { \"${f}\", asset_${idx}, sizeof(asset_${idx}), \"\\\"${etag}\\\"\", \"${mime}\" },\n")
math(EXPR idx "${idx} + 1")
endforeach()
set(LLAMA_UI_HAS_ASSETS 0)
if(n_assets GREATER 0)
set(LLAMA_UI_HAS_ASSETS 1)
endif()
set(N_ASSETS "${n_assets}")
set(USE_GZIP false)
if(use_gzip)
set(USE_GZIP true)
endif()
configure_file("${UI_TEMPLATE_DIR}/ui.h.in" "${UI_H}" @ONLY)
configure_file("${UI_TEMPLATE_DIR}/ui.cpp.in" "${UI_CPP}" @ONLY)
# Write the embed stamp last, after both generated files succeeded.
file(WRITE "${EMBED_STAMP}" "${fingerprint}")
message(STATUS "UI: embedded ${n_assets} assets")
endfunction()
function(npm_build_should_skip out_var)
set(${out_var} FALSE PARENT_SCOPE)
if(NOT EXISTS "${DIST_DIR}/index.html")
return()
endif()
if(EXISTS "${STAMP_FILE}")
return()
endif()
if(NOT EXISTS "${UI_SOURCE_DIR}/sources.cmake")
return()
endif()
include("${UI_SOURCE_DIR}/sources.cmake")
set(globs "")
foreach(g ${UI_SOURCE_GLOBS})
list(APPEND globs "${UI_SOURCE_DIR}/${g}")
endforeach()
file(GLOB_RECURSE sources ${globs})
foreach(f ${UI_SOURCE_FILES})
list(APPEND sources "${UI_SOURCE_DIR}/${f}")
endforeach()
file(TIMESTAMP "${DIST_DIR}/index.html" out_ts)
foreach(s ${sources})
if(NOT EXISTS "${s}")
continue()
endif()
file(TIMESTAMP "${s}" s_ts)
if(s_ts STRGREATER out_ts)
return()
endif()
endforeach()
set(${out_var} TRUE PARENT_SCOPE)
endfunction()
function(stage_sources)
if(EXISTS "${WORK_DIR}")
file(GLOB staged RELATIVE "${WORK_DIR}" "${WORK_DIR}/*")
list(REMOVE_ITEM staged "node_modules")
foreach(entry ${staged})
file(REMOVE_RECURSE "${WORK_DIR}/${entry}")
endforeach()
endif()
file(COPY "${UI_SOURCE_DIR}/"
DESTINATION "${WORK_DIR}"
NO_SOURCE_PERMISSIONS
PATTERN "node_modules" EXCLUDE
)
endfunction()
function(npm_build out_var)
set(${out_var} FALSE PARENT_SCOPE)
if(NOT EXISTS "${UI_SOURCE_DIR}/package.json")
message(STATUS "UI: ${UI_SOURCE_DIR}/package.json not found, skipping npm")
return()
endif()
npm_build_should_skip(skip)
if(skip)
message(STATUS "UI: npm output up-to-date, skipping build")
set(${out_var} TRUE PARENT_SCOPE)
return()
endif()
if(CMAKE_HOST_WIN32)
find_program(NPM_EXECUTABLE NAMES npm.cmd npm.bat npm)
else()
find_program(NPM_EXECUTABLE npm)
endif()
if(NOT NPM_EXECUTABLE)
message(STATUS "UI: npm not found, skipping npm build")
return()
endif()
stage_sources()
# npm writes node_modules/.package-lock.json on every successful install,
# so a package-lock.json newer than this marker means node_modules is stale
set(NPM_MARKER "${WORK_DIR}/node_modules/.package-lock.json")
set(need_install FALSE)
if(NOT EXISTS "${NPM_MARKER}")
set(need_install TRUE)
else()
file(TIMESTAMP "${WORK_DIR}/package-lock.json" lock_ts)
file(TIMESTAMP "${NPM_MARKER}" marker_ts)
if(lock_ts STRGREATER marker_ts)
set(need_install TRUE)
endif()
endif()
if(need_install)
message(STATUS "UI: running npm ci")
execute_process(
COMMAND ${NPM_EXECUTABLE} ci
WORKING_DIRECTORY "${WORK_DIR}"
RESULT_VARIABLE rc
ERROR_VARIABLE err
)
if(NOT rc EQUAL 0)
message(STATUS "UI: npm ci failed (${rc})")
message(STATUS " stderr: ${err}")
return()
endif()
endif()
file(MAKE_DIRECTORY "${DIST_DIR}")
message(STATUS "UI: running npm run build, output -> ${DIST_DIR}")
execute_process(
COMMAND ${CMAKE_COMMAND} -E env "LLAMA_UI_OUT_DIR=${DIST_DIR}" "LLAMA_UI_VERSION=${HF_VERSION}" "LLAMA_BUILD_NUMBER=${LLAMA_BUILD_NUMBER}"
${NPM_EXECUTABLE} run build
WORKING_DIRECTORY "${WORK_DIR}"
RESULT_VARIABLE rc
ERROR_VARIABLE err
)
if(NOT rc EQUAL 0)
message(STATUS "UI: npm run build failed (${rc})")
message(STATUS " stderr: ${err}")
return()
endif()
if(NOT EXISTS "${DIST_DIR}/index.html")
message(STATUS "UI: npm build finished but assets missing in ${DIST_DIR}")
return()
endif()
message(STATUS "UI: npm build succeeded")
file(REMOVE "${STAMP_FILE}")
set(${out_var} TRUE PARENT_SCOPE)
endfunction()
function(resolve_version out_var)
if(NOT "${HF_VERSION}" STREQUAL "")
set(${out_var} "${HF_VERSION}" PARENT_SCOPE)
return()
endif()
if(EXISTS "${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")
include("${LLAMA_SOURCE_DIR}/cmake/build-info.cmake")
if(NOT "${BUILD_NUMBER}" STREQUAL "" AND NOT BUILD_NUMBER EQUAL 0)
set(${out_var} "b${BUILD_NUMBER}" PARENT_SCOPE)
return()
endif()
endif()
set(${out_var} "" PARENT_SCOPE)
endfunction()
function(hf_download version out_var out_resolved)
set(${out_var} FALSE PARENT_SCOPE)
set(${out_resolved} "" PARENT_SCOPE)
set(archive "${UI_BINARY_DIR}/dist.tar.gz")
# Use HF_TOKEN to benefit from higher rate limits
set(auth_headers "")
if(DEFINED ENV{HF_TOKEN} AND NOT "$ENV{HF_TOKEN}" STREQUAL "")
list(APPEND auth_headers "HTTPHEADER" "Authorization: Bearer $ENV{HF_TOKEN}")
endif()
set(candidates "")
if(NOT "${version}" STREQUAL "")
list(APPEND candidates "${version}")
endif()
list(APPEND candidates "latest")
foreach(resolved ${candidates})
set(base "https://huggingface.co/buckets/${HF_BUCKET}/resolve/${resolved}")
message(STATUS "UI: downloading from ${resolved}: ${base}/dist.tar.gz")
# Fetch the checksum first: when the archive we already have matches
# it, the expensive download is skipped and only extraction repeats.
file(DOWNLOAD "${base}/dist.tar.gz.sha256?download=true" "${archive}.sha256"
STATUS status TIMEOUT 30 ${auth_headers}
)
list(GET status 0 rc)
if(NOT rc EQUAL 0)
list(GET status 1 errmsg)
message(STATUS "UI: download dist.tar.gz.sha256 from ${resolved} failed: ${errmsg}")
continue()
endif()
# Validate the sha256 checksum: reject anything that is not a full
# 64-hex-digit digest before touching the archive.
file(READ "${archive}.sha256" expected)
string(REGEX MATCH "^[0-9a-fA-F]+" expected "${expected}")
string(TOLOWER "${expected}" expected)
string(LENGTH "${expected}" expected_len)
if(NOT expected_len EQUAL 64)
message(STATUS "UI: invalid checksum from ${resolved}")
continue()
endif()
set(actual "")
if(EXISTS "${archive}")
file(SHA256 "${archive}" actual)
endif()
if("${actual}" STREQUAL "${expected}")
message(STATUS "UI: local dist.tar.gz matches checksum from ${resolved}, skipping download")
else()
file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}"
STATUS status TIMEOUT 300 ${auth_headers}
)
list(GET status 0 rc)
if(NOT rc EQUAL 0)
list(GET status 1 errmsg)
message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}")
continue()
endif()
file(SHA256 "${archive}" actual)
if(NOT "${actual}" STREQUAL "${expected}")
message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}")
continue()
endif()
endif()
# Remove the stamp with the dist tree it describes, together.
file(REMOVE "${STAMP_FILE}")
file(REMOVE_RECURSE "${DIST_DIR}")
file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")
if(NOT EXISTS "${DIST_DIR}/index.html")
message(STATUS "UI: archive from ${resolved} is missing required assets")
continue()
endif()
message(STATUS "UI: archive verified and extracted")
set(${out_var} TRUE PARENT_SCOPE)
set(${out_resolved} "${resolved}" PARENT_SCOPE)
return()
endforeach()
endfunction()
# ---------------------------------------------------------------------------
# 1. Priority 1: pre-built assets supplied in tools/ui/dist
# ---------------------------------------------------------------------------
if(EXISTS "${SRC_DIST_DIR}/index.html")
message(STATUS "UI: using pre-built assets from ${SRC_DIST_DIR}")
emit_files("${SRC_DIST_DIR}")
return()
endif()
# ---------------------------------------------------------------------------
# 2. Priority 2: npm build (if BUILD_UI=ON)
# ---------------------------------------------------------------------------
set(provisioned FALSE)
if(BUILD_UI)
# Resolve version from git build-info if not explicitly set
resolve_version(HF_VERSION)
npm_build(NPM_OK)
if(NPM_OK)
set(provisioned TRUE)
endif()
endif()
# ---------------------------------------------------------------------------
# 3. Priority 3: HF Bucket download (if npm did not produce assets and HF_ENABLED=ON)
# ---------------------------------------------------------------------------
if(NOT provisioned AND HF_ENABLED)
resolve_version(VERSION)
# Stamp a successful HF download: records bucket + requested version and
# lets later steps distinguish downloaded assets from locally built ones.
set(stamp_key "${HF_BUCKET}|${VERSION}")
set(stamp_ok FALSE)
if(EXISTS "${STAMP_FILE}" AND EXISTS "${DIST_DIR}/index.html" AND NOT "${VERSION}" STREQUAL "")
file(READ "${STAMP_FILE}" stamped)
string(STRIP "${stamped}" stamped)
if(stamped STREQUAL "${stamp_key}")
set(stamp_ok TRUE)
endif()
endif()
if(stamp_ok)
message(STATUS "UI: HF stamp matches '${stamp_key}', skipping HF fetch")
set(provisioned TRUE)
else()
hf_download("${VERSION}" HF_OK HF_RESOLVED)
if(HF_OK)
file(WRITE "${STAMP_FILE}" "${stamp_key}")
message(STATUS "UI: HF download succeeded, stamp updated (${stamp_key}, resolved: ${HF_RESOLVED})")
set(provisioned TRUE)
else()
message(STATUS "UI: HF download failed")
endif()
endif()
endif()
# ---------------------------------------------------------------------------
# 4. Fallback: warn about stale or missing assets, then emit whatever we have
# ---------------------------------------------------------------------------
if(NOT provisioned)
if(EXISTS "${DIST_DIR}/index.html")
message(WARNING "UI: provisioning failed; embedding stale assets from ${DIST_DIR}")
else()
message(WARNING "UI: no assets available - building without an embedded UI. "
"In a disconnected environment, download the pre-built UI "
"from a llama.cpp release at "
"https://github.com/ggml-org/llama.cpp/releases and "
"extract to tools/ui/dist.")
endif()
endif()
emit_files("${DIST_DIR}")
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
import logging
import os
import hashlib
logger = logging.getLogger("verify-checksum-models")
def sha256sum(file):
block_size = 16 * 1024 * 1024 # 16 MB block size
b = bytearray(block_size)
file_hash = hashlib.sha256()
mv = memoryview(b)
with open(file, 'rb', buffering=0) as f:
while True:
n = f.readinto(mv)
if not n:
break
file_hash.update(mv[:n])
return file_hash.hexdigest()
# Define the path to the llama directory (parent folder of script directory)
llama_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
# Define the file with the list of hashes and filenames
hash_list_file = os.path.join(llama_path, "SHA256SUMS")
# Check if the hash list file exists
if not os.path.exists(hash_list_file):
logger.error(f"Hash list file not found: {hash_list_file}")
exit(1)
# Read the hash file content and split it into an array of lines
with open(hash_list_file, "r") as f:
hash_list = f.read().splitlines()
# Create an array to store the results
results = []
# Loop over each line in the hash list
for line in hash_list:
# Split the line into hash and filename
hash_value, filename = line.split(" ")
# Get the full path of the file by joining the llama path and the filename
file_path = os.path.join(llama_path, filename)
# Informing user of the progress of the integrity check
logger.info(f"Verifying the checksum of {file_path}")
# Check if the file exists
if os.path.exists(file_path):
# Calculate the SHA256 checksum of the file using hashlib
file_hash = sha256sum(file_path)
# Compare the file hash with the expected hash
if file_hash == hash_value:
valid_checksum = "V"
file_missing = ""
else:
valid_checksum = ""
file_missing = ""
else:
valid_checksum = ""
file_missing = "X"
# Add the results to the array
results.append({
"filename": filename,
"valid checksum": valid_checksum,
"file missing": file_missing
})
# Print column headers for results table
print("filename".ljust(40) + "valid checksum".center(20) + "file missing".center(20)) # noqa: NP100
print("-" * 80) # noqa: NP100
# Output the results as a table
for r in results:
print(f"{r['filename']:40} {r['valid checksum']:^20} {r['file missing']:^20}") # noqa: NP100
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# initialize a new worktree from a branch name:
#
# - creates a new branch from current HEAD
# - creates a new worktree in a parent folder, suffixed with the branch name
#
# sample usage:
# ./scripts/wc2wt.sh gg/new-feature-foo-bar
# ./scripts/wc2wt.sh gg/new-feature-foo-bar opencode
# ./scripts/wc2wt.sh gg/new-feature-foo-bar "cmake -B build && cmake --build build"
# ./scripts/wc2wt.sh gg/new-feature-foo-bar "bash -l"
function usage() {
echo "usage: $0 <branch_name> [cmd]"
exit 1
}
# check we are in the right directory
if [[ ! -f "scripts/wc2wt.sh" ]]; then
echo "error: this script must be run from the root of the repository"
exit 1
fi
if [[ $# -lt 1 || $# -gt 2 ]]; then
usage
fi
BRANCH=$1
if [[ -z "$BRANCH" ]]; then
echo "error: branch name must not be empty"
exit 1
fi
dir=$(basename $(pwd))
# sanitize branch name for directory name (replace / with -)
dir_suffix=$(echo "$BRANCH" | tr '/' '-')
git worktree add "../$dir-$dir_suffix" "$BRANCH" || git worktree add -b "$BRANCH" "../$dir-$dir_suffix" HEAD
wt_path=$(cd "../$dir-$dir_suffix" && pwd)
echo "git worktree created in $wt_path"
cd "$wt_path"
if [[ $# -eq 2 ]]; then
echo "executing: $2"
eval "$2"
fi