mirror of
https://github.com/ROCm/composable_kernel.git
synced 2026-05-05 06:01:23 +00:00
[CK] [CK_TILE] Improve build and test time of CI with smart dependency parser (#5249) ## Motivation Existing dependency parser needs full build of tests to determine which tests are affected by code changes in a PR. This still takes 2-4 hours for building the tests which slows down the CI as the number of tests grow. To resolve this issue we implemented a smart dependency parser which uses CMake Configure to parse dependencies and build only the affected test cases. We have ensured that two approaches are available 1) CMake pre-build analysis for each PR to ensure fast build and test. 2) Ninja post-build analysis to enable full build for nightly tests. ## Technical Details ```bash ### 1. Configure the project with CMake cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. ### 2. Analyze dependencies (no build required!) python3 ../script/dependency-parser/main.py cmake-parse compile_commands.json build.ninja \ --workspace-root .. --output cmake_dependency_mapping.json --parallel 8 ### 3. Find tests affected by changes python3 ../script/dependency-parser/main.py select cmake_dependency_mapping.json origin/develop \ HEAD --test-prefix --output tests_to_run.json ### 4. Build only affected tests ninja $(jq -r '.executables[]' tests_to_run.json | tr '\n' ' ') ### 5. Run affected tests ctest -R "$(jq -r '.regex' tests_to_run.json)" ``` ### Jenkins Integration - Added `buildMode` to jenkinsfile to integrate both `selective` and `full` build methods ### Known Limitations ### 1. Build-Time Generated Headers (HIGH RISK) **Problem:** Files generated during the build process (e.g., via `add_custom_command`) cannot be analyzed before building. **Example:** ```cmake add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/generated/config.hpp COMMAND generate_config.sh DEPENDS template.hpp.in ) ``` **Impact:** If a source file includes `generated/config.hpp`, the dependency won't be detected until after building. **Mitigation:** - CK analysis shows **no generated headers** currently used - If generated headers are added in the future, they must be built first - Recommendation: Generate headers in CMake configure phase (not build phase) when possible ## Test Plan **1. Modified Files:** ``` include/ck_tile/ops/common.hpp include/ck_tile/ops/gemm.hpp include/ck_tile/ops/gemm/warp/warp_gemm.hpp ``` **2. Compare tests selected between `build.ninja` and `cmake-parse` methods** ## Test Result - 1. The test completed in 5-6 minutes finding about 8000+ executables that should be built. - 2. We selected a commit 5ccc1387ea which resulted in same 7 tests with both legacy and new methods. - PR | Legacy tests | Smart tests | Notes -- | -- | -- | -- 5261 | 453 | 455 | Only 2 tests (test_amdgcn_mma and test_amdgcn_sparse_mma) 5168 | 0 | 0 | Changes in dispatcher only. No CK tests invoked. 5249 | 0 | 0 | Changes to dependency parser. No CK tests invoked 5260 | 0 | 0 | Changes in dispatcher only. No CK tests invoked. 5174 | 1 | 1 | One test from FMHA affected by this PR in both cases 5383 | 0 | 0 | Changes are only in benchmark files. Did not trigger any tests 5445 | 1 | 1 | Changes are only to tests/ck_tile/gemm_streamk. Only triggered one streamk test in both cases. 5454 | 3 | 3 | Both methods identified same test_grouped_conv_bwd tests 5427 | 234 | 234 | Core infrastructure header changes. Detected exactly same tests 5388 | 85 | 85 | modifies warp-level GEMM operations (warp_gemm.hpp, warp_gemm_dispatcher.hpp). Correctly identified all the streamK gemm tests ## Submission Checklist - [x ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
392 lines
10 KiB
Bash
Executable File
392 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
# Local Smart Build Runner for ComposableKernel
|
|
# Run smart build workflow locally without Jenkins
|
|
|
|
set -e
|
|
|
|
# Colors for output
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Default values
|
|
PARALLEL=$(nproc)
|
|
BASE_REF="HEAD~1" # Previous commit (default for local testing)
|
|
TARGET_REF="HEAD" # Current state including uncommitted changes
|
|
BUILD_DIR="../../build"
|
|
CTEST_ONLY="--ctest-only"
|
|
WORKSPACE_ROOT="../.."
|
|
|
|
print_help() {
|
|
cat << 'HELP'
|
|
Usage: local_smart_build.sh [COMMAND] [OPTIONS]
|
|
|
|
Commands:
|
|
analyze Generate dependency map (step 1)
|
|
select Select affected tests (step 2)
|
|
build Build selected tests (step 3)
|
|
test Run selected tests (step 4)
|
|
all Run complete workflow (analyze → select → build → test)
|
|
stats Show statistics about test selection
|
|
clean Clean generated files
|
|
|
|
Options:
|
|
-b, --base-ref REF Base ref to compare against (default: HEAD~1)
|
|
-t, --target-ref REF Target ref to compare (default: HEAD)
|
|
-j, --parallel NUM Parallel jobs for analysis (default: nproc)
|
|
--build-dir DIR Build directory relative to script (default: ../../build)
|
|
--no-ctest-only Include all executables (benchmarks, examples)
|
|
-h, --help Show this help
|
|
|
|
Examples:
|
|
# Test uncommitted changes vs last commit (default)
|
|
./local_smart_build.sh all
|
|
|
|
# Test current branch vs develop
|
|
./local_smart_build.sh select -b origin/develop -t HEAD
|
|
|
|
# Test specific commit range
|
|
./local_smart_build.sh all -b abc123 -t def456
|
|
|
|
# Step by step
|
|
./local_smart_build.sh analyze
|
|
./local_smart_build.sh select
|
|
./local_smart_build.sh build
|
|
./local_smart_build.sh test
|
|
|
|
# Include all executables (not just tests)
|
|
./local_smart_build.sh all --no-ctest-only
|
|
|
|
Default behavior (no options):
|
|
Compares HEAD~1 (previous commit) vs HEAD (current state + uncommitted changes)
|
|
This tests your latest changes including work-in-progress.
|
|
|
|
File locations (in build directory):
|
|
- compile_commands.json (CMake generated)
|
|
- build.ninja (CMake generated)
|
|
- enhanced_dependency_mapping.json (analyze output)
|
|
- tests_to_run.json (select output)
|
|
HELP
|
|
}
|
|
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
check_prerequisites() {
|
|
log_info "Checking prerequisites..."
|
|
|
|
if ! command -v cmake &> /dev/null; then
|
|
log_error "cmake not found. Please install CMake."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v ninja &> /dev/null; then
|
|
log_error "ninja not found. Please install Ninja build system."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v python3 &> /dev/null; then
|
|
log_error "python3 not found. Please install Python 3."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq &> /dev/null; then
|
|
log_error "jq not found. Please install jq for JSON processing."
|
|
exit 1
|
|
fi
|
|
|
|
log_info "All prerequisites found ✓"
|
|
}
|
|
|
|
cmd_analyze() {
|
|
log_info "Step 1: Generating dependency map..."
|
|
|
|
cd "$BUILD_DIR" || exit 1
|
|
|
|
# Always reconfigure CMake to ensure fresh compile_commands.json
|
|
log_info "Running CMake configure to generate fresh compile_commands.json..."
|
|
|
|
# Use CMAKE flags similar to the dev preset and README recommendations
|
|
cmake -G Ninja \
|
|
-DCMAKE_PREFIX_PATH=/opt/rocm \
|
|
-DCMAKE_CXX_COMPILER=/opt/rocm/bin/hipcc \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
|
-DBUILD_DEV=ON \
|
|
"$WORKSPACE_ROOT"
|
|
|
|
if [ ! -f "build.ninja" ]; then
|
|
log_error "build.ninja not found after CMake configure"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Analyzing dependencies with $PARALLEL workers (this takes ~2 minutes)..."
|
|
python3 "$WORKSPACE_ROOT/script/dependency-parser/main.py" cmake-parse \
|
|
compile_commands.json \
|
|
build.ninja \
|
|
--workspace-root "$WORKSPACE_ROOT" \
|
|
--parallel "$PARALLEL" \
|
|
--output enhanced_dependency_mapping.json
|
|
|
|
log_info "Dependency map generated: enhanced_dependency_mapping.json ✓"
|
|
|
|
# Show stats
|
|
local num_files=$(jq '.file_to_executables | length' enhanced_dependency_mapping.json)
|
|
log_info "Mapped $num_files files to executables"
|
|
}
|
|
|
|
cmd_select() {
|
|
log_info "Step 2: Selecting affected tests..."
|
|
|
|
cd "$BUILD_DIR" || exit 1
|
|
|
|
if [ ! -f "enhanced_dependency_mapping.json" ]; then
|
|
log_error "Dependency map not found. Run 'analyze' first."
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Comparing $BASE_REF → $TARGET_REF..."
|
|
python3 "$WORKSPACE_ROOT/script/dependency-parser/main.py" select \
|
|
enhanced_dependency_mapping.json \
|
|
"$BASE_REF" \
|
|
"$TARGET_REF" \
|
|
$CTEST_ONLY \
|
|
--output tests_to_run.json
|
|
|
|
# Show statistics
|
|
local num_files=$(jq -r '.statistics.total_changed_files' tests_to_run.json)
|
|
local num_tests=$(jq -r '.statistics.total_affected_executables' tests_to_run.json)
|
|
local num_chunks=$(jq -r '.statistics.num_regex_chunks' tests_to_run.json)
|
|
|
|
log_info "Test selection complete ✓"
|
|
echo " Changed files: $num_files"
|
|
echo " Affected tests: $num_tests"
|
|
echo " Regex chunks: $num_chunks"
|
|
|
|
if [ "$num_tests" -eq 0 ]; then
|
|
log_warn "No tests affected by your changes"
|
|
fi
|
|
}
|
|
|
|
cmd_build() {
|
|
log_info "Step 3: Building affected tests..."
|
|
|
|
cd "$BUILD_DIR" || exit 1
|
|
|
|
if [ ! -f "tests_to_run.json" ]; then
|
|
log_error "Test selection not found. Run 'select' first."
|
|
exit 1
|
|
fi
|
|
|
|
local num_tests=$(jq -r '.statistics.total_affected_executables' tests_to_run.json)
|
|
|
|
if [ "$num_tests" -eq 0 ]; then
|
|
log_warn "No tests to build"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Building $num_tests test executables..."
|
|
|
|
local targets=$(jq -r '.executables[]' tests_to_run.json | tr '\n' ' ')
|
|
|
|
if [ -n "$targets" ]; then
|
|
ninja -j"$PARALLEL" $targets
|
|
log_info "Build complete ✓"
|
|
else
|
|
log_warn "No targets to build"
|
|
fi
|
|
}
|
|
|
|
cmd_test() {
|
|
log_info "Step 4: Running affected tests..."
|
|
|
|
cd "$BUILD_DIR" || exit 1
|
|
|
|
if [ ! -f "tests_to_run.json" ]; then
|
|
log_error "Test selection not found. Run 'select' first."
|
|
exit 1
|
|
fi
|
|
|
|
local num_chunks=$(jq -r '.regex_chunks | length' tests_to_run.json)
|
|
|
|
if [ "$num_chunks" -eq 0 ]; then
|
|
log_warn "No tests to run"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Running tests in $num_chunks chunk(s)..."
|
|
|
|
if [ "$num_chunks" -eq 1 ]; then
|
|
# Single chunk - simple case
|
|
local regex=$(jq -r '.regex_chunks[0]' tests_to_run.json)
|
|
ctest -R "$regex" --output-on-failure
|
|
else
|
|
# Multiple chunks
|
|
for i in $(seq 0 $((num_chunks - 1))); do
|
|
log_info "Running test chunk $((i + 1))/$num_chunks"
|
|
local regex=$(jq -r ".regex_chunks[$i]" tests_to_run.json)
|
|
ctest -R "$regex" --output-on-failure
|
|
done
|
|
fi
|
|
|
|
log_info "All tests complete ✓"
|
|
}
|
|
|
|
cmd_all() {
|
|
log_info "Running complete smart build workflow..."
|
|
log_info "Testing changes: $BASE_REF → $TARGET_REF"
|
|
echo ""
|
|
|
|
cmd_analyze
|
|
echo ""
|
|
|
|
cmd_select
|
|
echo ""
|
|
|
|
cmd_build
|
|
echo ""
|
|
|
|
cmd_test
|
|
echo ""
|
|
|
|
log_info "Smart build workflow complete! ✓"
|
|
}
|
|
|
|
cmd_stats() {
|
|
log_info "Smart Build Statistics"
|
|
echo ""
|
|
|
|
cd "$BUILD_DIR" || exit 1
|
|
|
|
if [ -f "enhanced_dependency_mapping.json" ]; then
|
|
echo "Dependency Map:"
|
|
local num_files=$(jq '.file_to_executables | length' enhanced_dependency_mapping.json)
|
|
echo " Total files tracked: $num_files"
|
|
|
|
# Check core.hpp as example
|
|
if jq -e '.file_to_executables["include/ck_tile/core.hpp"]' enhanced_dependency_mapping.json &> /dev/null; then
|
|
local core_deps=$(jq '.file_to_executables["include/ck_tile/core.hpp"] | length' enhanced_dependency_mapping.json)
|
|
echo " Executables depending on core.hpp: $core_deps"
|
|
fi
|
|
else
|
|
log_warn "Dependency map not found. Run 'analyze' first."
|
|
fi
|
|
|
|
echo ""
|
|
|
|
if [ -f "tests_to_run.json" ]; then
|
|
echo "Test Selection:"
|
|
jq '.statistics' tests_to_run.json
|
|
|
|
echo ""
|
|
echo "Changed files:"
|
|
jq -r '.changed_files[]' tests_to_run.json
|
|
|
|
echo ""
|
|
echo "Sample affected tests (first 10):"
|
|
jq -r '.executables[:10][]' tests_to_run.json
|
|
else
|
|
log_warn "Test selection not found. Run 'select' first."
|
|
fi
|
|
}
|
|
|
|
cmd_clean() {
|
|
log_info "Cleaning generated smart build files..."
|
|
|
|
cd "$BUILD_DIR" || exit 1
|
|
|
|
rm -f enhanced_dependency_mapping.json tests_to_run.json build_mode.env
|
|
|
|
log_info "Clean complete ✓"
|
|
}
|
|
|
|
# Parse command line arguments
|
|
COMMAND=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
analyze|select|build|test|all|stats|clean)
|
|
COMMAND="$1"
|
|
shift
|
|
;;
|
|
-b|--base-ref)
|
|
BASE_REF="$2"
|
|
shift 2
|
|
;;
|
|
-t|--target-ref)
|
|
TARGET_REF="$2"
|
|
shift 2
|
|
;;
|
|
-j|--parallel)
|
|
PARALLEL="$2"
|
|
shift 2
|
|
;;
|
|
--build-dir)
|
|
BUILD_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--no-ctest-only)
|
|
CTEST_ONLY=""
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
print_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
print_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validate command
|
|
if [ -z "$COMMAND" ]; then
|
|
log_error "No command specified"
|
|
print_help
|
|
exit 1
|
|
fi
|
|
|
|
# Main execution
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
log_info "Script location: $SCRIPT_DIR"
|
|
|
|
cd "$SCRIPT_DIR" || exit 1
|
|
|
|
check_prerequisites
|
|
|
|
case "$COMMAND" in
|
|
analyze)
|
|
cmd_analyze
|
|
;;
|
|
select)
|
|
cmd_select
|
|
;;
|
|
build)
|
|
cmd_build
|
|
;;
|
|
test)
|
|
cmd_test
|
|
;;
|
|
all)
|
|
cmd_all
|
|
;;
|
|
stats)
|
|
cmd_stats
|
|
;;
|
|
clean)
|
|
cmd_clean
|
|
;;
|
|
esac
|