diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b173614 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + format-check: + name: clang-format check + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Install clang-format + run: sudo apt-get update && sudo apt-get install -y clang-format + - name: Check formatting + run: make check-format + + test: + name: Unit tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libnvidia-ml-dev libgtest-dev + - name: Build and run unit tests + run: make test diff --git a/Makefile b/Makefile index 9a3b453..0ec3cd4 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,28 @@ CXX = g++ CXXFLAGS = -std=c++11 -Wall -Wextra -O2 NVML_LIB = /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 -CUDA_INCLUDE = -I/usr/local/cuda-12.9/targets/x86_64-linux/include -CUDA_LIB = -L/usr/local/cuda-12.9/targets/x86_64-linux/lib -lcudart +CUDA_INCLUDE = -I/usr/local/cuda/targets/x86_64-linux/include +CUDA_LIB = -L/usr/local/cuda/targets/x86_64-linux/lib -lcudart +GTEST_LIBS = -lgtest -lgtest_main -lpthread BUILD_DIR = build MONITOR_TARGET = $(BUILD_DIR)/nvlink_monitor EXAMPLE_TARGET = $(BUILD_DIR)/nvlink_bw_test +TEST_TARGET = $(BUILD_DIR)/run_tests -MONITOR_SOURCES = monitor/nvlink_monitor.cpp -MONITOR_HEADERS = monitor/nvlink_monitor.h -EXAMPLE_SOURCES = example/nvlink_bw_test.cpp +MONITOR_SOURCES = monitor/nvlink_monitor.cpp monitor/bandwidth_calc.cpp monitor/arg_parser.cpp +MONITOR_HEADERS = monitor/nvlink_monitor.h monitor/bandwidth_calc.h monitor/arg_parser.h +EXAMPLE_SOURCES = example/nvlink_bw_test.cpp example/bw_stats.cpp example/arg_parser.cpp +EXAMPLE_HEADERS = example/bw_stats.h example/arg_parser.h + +TEST_SOURCES = test/test_bandwidth_calc.cpp test/test_arg_parser.cpp test/test_bw_stats.cpp \ + test/test_bw_test_args.cpp \ + monitor/bandwidth_calc.cpp monitor/arg_parser.cpp example/bw_stats.cpp example/arg_parser.cpp +TEST_HEADERS = monitor/bandwidth_calc.h monitor/arg_parser.h example/bw_stats.h example/arg_parser.h monitor/nvlink_monitor.h + +# Sources checked by clang-format (all C++ sources and headers) +FORMAT_SOURCES = $(MONITOR_SOURCES) $(MONITOR_HEADERS) \ + $(EXAMPLE_SOURCES) $(EXAMPLE_HEADERS) test/*.cpp # Default target all: $(MONITOR_TARGET) $(EXAMPLE_TARGET) @@ -18,21 +30,38 @@ all: $(MONITOR_TARGET) $(EXAMPLE_TARGET) # Compile the monitor program $(MONITOR_TARGET): $(MONITOR_SOURCES) $(MONITOR_HEADERS) @mkdir -p $(BUILD_DIR) - $(CXX) $(CXXFLAGS) -o $(MONITOR_TARGET) $(MONITOR_SOURCES) $(NVML_LIB) + $(CXX) $(CXXFLAGS) $(CUDA_INCLUDE) -o $(MONITOR_TARGET) $(MONITOR_SOURCES) $(NVML_LIB) # Compile the example program -$(EXAMPLE_TARGET): $(EXAMPLE_SOURCES) +$(EXAMPLE_TARGET): $(EXAMPLE_SOURCES) $(EXAMPLE_HEADERS) @mkdir -p $(BUILD_DIR) $(CXX) $(CXXFLAGS) $(CUDA_INCLUDE) -o $(EXAMPLE_TARGET) $(EXAMPLE_SOURCES) $(CUDA_LIB) +# Compile and run unit tests (no GPU/NVML/CUDA required to link) +$(TEST_TARGET): $(TEST_SOURCES) $(TEST_HEADERS) + @mkdir -p $(BUILD_DIR) + $(CXX) $(CXXFLAGS) -I. $(CUDA_INCLUDE) -o $(TEST_TARGET) $(TEST_SOURCES) $(GTEST_LIBS) + # Build only monitor monitor: $(MONITOR_TARGET) # Build only example example: $(EXAMPLE_TARGET) +# Build and run unit tests +test: $(TEST_TARGET) + ./$(TEST_TARGET) + # Clean build artifacts clean: rm -rf $(BUILD_DIR) -.PHONY: all monitor example clean \ No newline at end of file +# Apply clang-format in place to all sources +format: + clang-format -i $(FORMAT_SOURCES) + +# Fail if any source is not clang-format-clean (for CI) +check-format: + @clang-format --dry-run --Werror $(FORMAT_SOURCES) + +.PHONY: all monitor example test format check-format clean diff --git a/README.md b/README.md index ea8a0d3..9f28978 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,18 @@ A comprehensive toolkit for monitoring and testing NVIDIA NVLink bandwidth and s ``` NvLinkMonitor/ ├── monitor/ # NVLink monitoring tool -│ ├── nvlink_monitor.cpp # Main monitoring implementation -│ └── nvlink_monitor.h # Monitoring tool headers +│ ├── nvlink_monitor.{cpp,h} # Monitor class + main() +│ ├── bandwidth_calc.{cpp,h} # Bandwidth calculation logic (extracted) +│ └── arg_parser.{cpp,h} # CLI argument parsing (extracted) ├── example/ # NVLink bandwidth testing tool -│ └── nvlink_bw_test.cpp # Bandwidth test implementation -├── build/ # Build output directory -├── Makefile # Main build configuration +│ ├── nvlink_bw_test.cpp # Bandwidth test main() +│ ├── bw_stats.{cpp,h} # Bandwidth stats aggregation (extracted) +│ └── arg_parser.{cpp,h} # CLI argument parsing (extracted) +├── test/ # GoogleTest unit tests (no GPU needed) +├── .github/workflows/ci.yml # CI: format check + unit tests +├── Makefile # Build configuration ├── install-deps.sh # Dependency installation script +├── .clang-format # clang-format style (Google, 4-space) └── README.md # This file ``` @@ -69,6 +74,23 @@ The executables will be created in the `build/` directory: - `build/nvlink_monitor` - NVLink monitoring tool - `build/nvlink_bw_test` - NVLink bandwidth test tool +### 🧪 Testing + +Unit tests use [GoogleTest](https://github.com/google/googletest) and run **without a GPU** — the test binary links only the extracted pure-logic modules (`bandwidth_calc`, `arg_parser`, `bw_stats`) plus gtest, with no NVML/CUDA library dependencies. + +```bash +# Run unit tests (builds the test binary first) +make test + +# Apply clang-format in place to all sources +make format + +# Fail if any source is not clang-format-clean (CI gate) +make check-format +``` + +CI (`.github/workflows/ci.yml`) runs `make check-format` and `make test` on ubuntu-22.04 for every push and pull request. The GPU binaries themselves are not built in CI (they require a CUDA toolkit and driver). + ## 🧩 Components ### 1. 📊 NVLink Monitor (`monitor/`) @@ -82,27 +104,27 @@ A real-time monitoring tool for NVLink bandwidth and status. #### Continuous monitoring: ```bash -./build/nvlink_monitor -continuous true +./build/nvlink_monitor --continuous true ``` #### Single monitoring: ```bash -./build/nvlink_monitor -continuous false +./build/nvlink_monitor --continuous false ``` #### Custom interval (e.g., 0.5 seconds): ```bash -./build/nvlink_monitor -interval 0.5 +./build/nvlink_monitor --interval 0.5 ``` #### Detailed NvLink output: ```bash -./build/nvlink_monitor -verbose +./build/nvlink_monitor --verbose ``` #### Combined options: ```bash -./build/nvlink_monitor -continuous false -interval 0.5 -verbose +./build/nvlink_monitor --continuous false --interval 0.5 --verbose ``` #### Output to file: diff --git a/example/arg_parser.cpp b/example/arg_parser.cpp new file mode 100644 index 0000000..ac40046 --- /dev/null +++ b/example/arg_parser.cpp @@ -0,0 +1,129 @@ +#include "arg_parser.h" + +#include +#include + +#include +#include +#include + +namespace { + +// Parses a base-10 integer from `s` into `out`. Returns false and sets `err` +// on parse failure (non-numeric trailing chars) or ERANGE overflow. +bool parseLong(const char* s, long& out, std::string& err, const char* name) { + char* end = nullptr; + errno = 0; + out = std::strtol(s, &end, 10); + if (errno == ERANGE) { + err = std::string(name) + " out of range"; + return false; + } + if (end == s || *end != '\0') { + err = std::string("invalid ") + name + ": " + s; + return false; + } + return true; +} + +} // namespace + +TestConfig parseBwTestArgs(int argc, char* argv[]) { + TestConfig config; + + static struct option long_options[] = { + {"iterations", required_argument, 0, 'i'}, + {"buffer-size", required_argument, 0, 'b'}, + {"src-gpu", required_argument, 0, 's'}, + {"dst-gpu", required_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0}}; + + // Reset getopt's global state so repeated calls (e.g. in tests) re-parse + // from the beginning. glibc resets internal state when optind is set to 0. + optind = 0; + + int opt; + int option_index = 0; + + while ((opt = getopt_long(argc, argv, "i:b:s:d:h", long_options, + &option_index)) != -1) { + switch (opt) { + case 'i': { + long val; + if (!parseLong(optarg, val, config.errorMessage, + "iterations")) { + config.ok = false; + return config; + } + if (val <= 0) { + config.ok = false; + config.errorMessage = "iterations must be positive"; + return config; + } + config.iterations = static_cast(val); + break; + } + case 'b': { + long val; + if (!parseLong(optarg, val, config.errorMessage, + "buffer size")) { + config.ok = false; + return config; + } + if (val <= 0) { + config.ok = false; + config.errorMessage = "buffer size must be positive"; + return config; + } + config.buffer_size_mb = static_cast(val); + break; + } + case 's': { + long val; + if (!parseLong(optarg, val, config.errorMessage, + "source GPU ID")) { + config.ok = false; + return config; + } + if (val < 0) { + config.ok = false; + config.errorMessage = "source GPU ID must be non-negative"; + return config; + } + config.src_gpu_id = static_cast(val); + break; + } + case 'd': { + long val; + if (!parseLong(optarg, val, config.errorMessage, + "destination GPU ID")) { + config.ok = false; + return config; + } + if (val < 0) { + config.ok = false; + config.errorMessage = + "destination GPU ID must be non-negative"; + return config; + } + config.dst_gpu_id = static_cast(val); + break; + } + case 'h': + config.help = true; + break; + case '?': + // getopt already printed an error message to stderr. + config.ok = false; + config.errorMessage = "invalid command-line arguments"; + return config; + default: + config.ok = false; + config.errorMessage = "unexpected option"; + return config; + } + } + + return config; +} diff --git a/example/arg_parser.h b/example/arg_parser.h new file mode 100644 index 0000000..dafeeaa --- /dev/null +++ b/example/arg_parser.h @@ -0,0 +1,22 @@ +#ifndef NVLINK_BW_TEST_ARG_PARSER_H +#define NVLINK_BW_TEST_ARG_PARSER_H + +#include +#include + +// Parsed command-line arguments for nvlink_bw_test. +struct TestConfig { + int iterations = 100; + size_t buffer_size_mb = 1000; + int src_gpu_id = 0; + int dst_gpu_id = 1; + bool help = false; + bool ok = true; + std::string errorMessage; +}; + +// Parses nvlink_bw_test command-line arguments. +// On error, sets ok=false and errorMessage (does not print or exit). +TestConfig parseBwTestArgs(int argc, char* argv[]); + +#endif // NVLINK_BW_TEST_ARG_PARSER_H diff --git a/example/bw_stats.cpp b/example/bw_stats.cpp new file mode 100644 index 0000000..42c3d98 --- /dev/null +++ b/example/bw_stats.cpp @@ -0,0 +1,28 @@ +#include "bw_stats.h" + +#include +#include + +BandwidthStats computeBandwidthStats(const std::vector& copyTimesMs, + size_t bufferSizeMb) { + BandwidthStats stats{0.0, 0.0, 0.0, 0.0, false}; + + if (copyTimesMs.empty()) { + return stats; + } + + double avgTime = + std::accumulate(copyTimesMs.begin(), copyTimesMs.end(), 0.0) / + copyTimesMs.size(); + double minTime = *std::min_element(copyTimesMs.begin(), copyTimesMs.end()); + double maxTime = *std::max_element(copyTimesMs.begin(), copyTimesMs.end()); + + // bufferSizeMb is MiB; /1024.0 gives GiB. Time is ms; /1000.0 gives s. + stats.avgGiBps = (bufferSizeMb / 1024.0) / (avgTime / 1000.0); + stats.minGiBps = (bufferSizeMb / 1024.0) / (maxTime / 1000.0); + stats.maxGiBps = (bufferSizeMb / 1024.0) / (minTime / 1000.0); + stats.avgLatencyMs = avgTime; + stats.valid = true; + + return stats; +} diff --git a/example/bw_stats.h b/example/bw_stats.h new file mode 100644 index 0000000..898d7ec --- /dev/null +++ b/example/bw_stats.h @@ -0,0 +1,24 @@ +#ifndef NVLINK_BW_TEST_STATS_H +#define NVLINK_BW_TEST_STATS_H + +#include +#include + +// Aggregated bandwidth statistics for a series of copy timings. +// Values are GiB/s (bufferSizeMb is MiB, divided by 1024 to get GiB). +struct BandwidthStats { + double avgGiBps; + double minGiBps; + double maxGiBps; + double avgLatencyMs; + bool valid; +}; + +// Computes avg/min/max bandwidth (GiB/s) and avg latency (ms) from a +// vector of per-iteration copy times (in ms). Returns valid=false on +// empty input. min/max bandwidth are derived from max/min time +// respectively (slowest copy = lowest bandwidth). +BandwidthStats computeBandwidthStats(const std::vector& copyTimesMs, + size_t bufferSizeMb); + +#endif // NVLINK_BW_TEST_STATS_H diff --git a/example/nvlink_bw_test.cpp b/example/nvlink_bw_test.cpp index eafee23..83d3bc4 100644 --- a/example/nvlink_bw_test.cpp +++ b/example/nvlink_bw_test.cpp @@ -1,17 +1,19 @@ #include -#include -#include + #include #include #include -#include -#include +#include +#include #include -static bool checkCudaErrorReturn(cudaError_t result, const char *message) { +#include "arg_parser.h" +#include "bw_stats.h" + +static bool checkCudaErrorReturn(cudaError_t result, const char* message) { if (result != cudaSuccess) { std::cerr << message << " (Error code: " << result << " - " - << cudaGetErrorString(result) << ")" << std::endl; + << cudaGetErrorString(result) << ")" << std::endl; return false; } return true; @@ -19,20 +21,19 @@ static bool checkCudaErrorReturn(cudaError_t result, const char *message) { bool checkP2PSupport(int src_gpu, int dst_gpu) { int accessSupported; - cudaError_t err = cudaDeviceGetP2PAttribute(&accessSupported, - cudaDevP2PAttrAccessSupported, - src_gpu, dst_gpu); - + cudaError_t err = cudaDeviceGetP2PAttribute( + &accessSupported, cudaDevP2PAttrAccessSupported, src_gpu, dst_gpu); + if (!checkCudaErrorReturn(err, "Failed to check P2P support")) { return false; } - + if (accessSupported) { - std::cout << "✓ P2P access supported between GPU " << src_gpu + std::cout << "✓ P2P access supported between GPU " << src_gpu << " and GPU " << dst_gpu << std::endl; return true; } else { - std::cout << "✗ P2P access not supported between GPU " << src_gpu + std::cout << "✗ P2P access not supported between GPU " << src_gpu << " and GPU " << dst_gpu << std::endl; return false; } @@ -43,26 +44,30 @@ bool enableP2PAccess(int src_gpu, int dst_gpu) { if (!checkCudaErrorReturn(err, "Failed to set source device")) { return false; } - + err = cudaDeviceEnablePeerAccess(dst_gpu, 0); if (err != cudaSuccess && err != cudaErrorPeerAccessAlreadyEnabled) { - if (!checkCudaErrorReturn(err, "Failed to enable P2P access from source to destination GPU")) { + if (!checkCudaErrorReturn( + err, + "Failed to enable P2P access from source to destination GPU")) { return false; } } - + err = cudaSetDevice(dst_gpu); if (!checkCudaErrorReturn(err, "Failed to set destination device")) { return false; } - + err = cudaDeviceEnablePeerAccess(src_gpu, 0); if (err != cudaSuccess && err != cudaErrorPeerAccessAlreadyEnabled) { - if (!checkCudaErrorReturn(err, "Failed to enable P2P access from destination to source GPU")) { + if (!checkCudaErrorReturn( + err, + "Failed to enable P2P access from destination to source GPU")) { return false; } } - + std::cout << "✓ P2P access enabled between GPUs" << std::endl; return true; } @@ -73,331 +78,320 @@ void printGPUInfo(int src_gpu, int dst_gpu) { if (!checkCudaErrorReturn(err, "Failed to get device count")) { return; } - + std::cout << "GPU Information:" << std::endl; - std::cout << "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" << std::endl; - + std::cout << "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + "━━━━━━━" + << std::endl; + // Print source GPU information if (src_gpu < deviceCount) { cudaDeviceProp prop; err = cudaGetDeviceProperties(&prop, src_gpu); - if (checkCudaErrorReturn(err, "Failed to get source device properties")) { - std::cout << "Source GPU " << src_gpu << ": " << prop.name << std::endl; - std::cout << " ├─ Memory: " << prop.totalGlobalMem / (1024*1024*1024) << " GB" << std::endl; - std::cout << " ├─ Compute Capability: " << prop.major << "." << prop.minor << std::endl; - std::cout << " ├─ Max Threads per Block: " << prop.maxThreadsPerBlock << std::endl; - std::cout << " ├─ Max Threads per SM: " << prop.maxThreadsPerMultiProcessor << std::endl; - std::cout << " └─ Number of SMs: " << prop.multiProcessorCount << std::endl; + if (checkCudaErrorReturn(err, + "Failed to get source device properties")) { + std::cout << "Source GPU " << src_gpu << ": " << prop.name + << std::endl; + std::cout << " ├─ Memory: " + << prop.totalGlobalMem / (1024 * 1024 * 1024) << " GB" + << std::endl; + std::cout << " ├─ Compute Capability: " << prop.major << "." + << prop.minor << std::endl; + std::cout << " ├─ Max Threads per Block: " + << prop.maxThreadsPerBlock << std::endl; + std::cout << " ├─ Max Threads per SM: " + << prop.maxThreadsPerMultiProcessor << std::endl; + std::cout << " └─ Number of SMs: " << prop.multiProcessorCount + << std::endl; } } - + // Print destination GPU information if (dst_gpu < deviceCount) { cudaDeviceProp prop; err = cudaGetDeviceProperties(&prop, dst_gpu); - if (checkCudaErrorReturn(err, "Failed to get destination device properties")) { - std::cout << "Destination GPU " << dst_gpu << ": " << prop.name << std::endl; - std::cout << " ├─ Memory: " << prop.totalGlobalMem / (1024*1024*1024) << " GB" << std::endl; - std::cout << " ├─ Compute Capability: " << prop.major << "." << prop.minor << std::endl; - std::cout << " ├─ Max Threads per Block: " << prop.maxThreadsPerBlock << std::endl; - std::cout << " ├─ Max Threads per SM: " << prop.maxThreadsPerMultiProcessor << std::endl; - std::cout << " └─ Number of SMs: " << prop.multiProcessorCount << std::endl; + if (checkCudaErrorReturn( + err, "Failed to get destination device properties")) { + std::cout << "Destination GPU " << dst_gpu << ": " << prop.name + << std::endl; + std::cout << " ├─ Memory: " + << prop.totalGlobalMem / (1024 * 1024 * 1024) << " GB" + << std::endl; + std::cout << " ├─ Compute Capability: " << prop.major << "." + << prop.minor << std::endl; + std::cout << " ├─ Max Threads per Block: " + << prop.maxThreadsPerBlock << std::endl; + std::cout << " ├─ Max Threads per SM: " + << prop.maxThreadsPerMultiProcessor << std::endl; + std::cout << " └─ Number of SMs: " << prop.multiProcessorCount + << std::endl; } } std::cout << std::endl; } -double measureCopyTime(void* dst_ptr, void* src_ptr, size_t size_bytes, - int src_gpu, cudaMemcpyKind copy_kind) { - if (!checkCudaErrorReturn(cudaSetDevice(src_gpu), "Failed to set device for copy measurement")) { +double measureCopyTime(void* dst_ptr, void* src_ptr, size_t size_bytes, + int src_gpu, cudaMemcpyKind copy_kind) { + if (!checkCudaErrorReturn(cudaSetDevice(src_gpu), + "Failed to set device for copy measurement")) { return -1.0; } - + cudaEvent_t start, stop; - if (!checkCudaErrorReturn(cudaEventCreate(&start), "Failed to create start event")) { + if (!checkCudaErrorReturn(cudaEventCreate(&start), + "Failed to create start event")) { return -1.0; } - - if (!checkCudaErrorReturn(cudaEventCreate(&stop), "Failed to create stop event")) { + + if (!checkCudaErrorReturn(cudaEventCreate(&stop), + "Failed to create stop event")) { cudaEventDestroy(start); return -1.0; } - - if (!checkCudaErrorReturn(cudaDeviceSynchronize(), "Failed to synchronize device")) { + + if (!checkCudaErrorReturn(cudaDeviceSynchronize(), + "Failed to synchronize device")) { cudaEventDestroy(start); cudaEventDestroy(stop); return -1.0; } - - if (!checkCudaErrorReturn(cudaEventRecord(start), "Failed to record start event")) { + + if (!checkCudaErrorReturn(cudaEventRecord(start), + "Failed to record start event")) { cudaEventDestroy(start); cudaEventDestroy(stop); return -1.0; } - - if (!checkCudaErrorReturn(cudaMemcpy(dst_ptr, src_ptr, size_bytes, copy_kind), "Copy failed")) { + + if (!checkCudaErrorReturn( + cudaMemcpy(dst_ptr, src_ptr, size_bytes, copy_kind), + "Copy failed")) { cudaEventDestroy(start); cudaEventDestroy(stop); return -1.0; } - - if (!checkCudaErrorReturn(cudaEventRecord(stop), "Failed to record stop event")) { + + if (!checkCudaErrorReturn(cudaEventRecord(stop), + "Failed to record stop event")) { cudaEventDestroy(start); cudaEventDestroy(stop); return -1.0; } - - if (!checkCudaErrorReturn(cudaEventSynchronize(stop), "Failed to synchronize stop event")) { + + if (!checkCudaErrorReturn(cudaEventSynchronize(stop), + "Failed to synchronize stop event")) { cudaEventDestroy(start); cudaEventDestroy(stop); return -1.0; } - + float time_ms; - if (!checkCudaErrorReturn(cudaEventElapsedTime(&time_ms, start, stop), "Failed to get elapsed time")) { + if (!checkCudaErrorReturn(cudaEventElapsedTime(&time_ms, start, stop), + "Failed to get elapsed time")) { cudaEventDestroy(start); cudaEventDestroy(stop); return -1.0; } - + cudaEventDestroy(start); cudaEventDestroy(stop); - + return time_ms; } -void testCopyPerformance(int src_gpu, int dst_gpu, - size_t buffer_size_mb, int iterations) { - std::cout << "Buffer size: " << buffer_size_mb << " MB | Iterations: " << iterations << std::endl; - +void testCopyPerformance(int src_gpu, int dst_gpu, size_t buffer_size_mb, + int iterations) { + std::cout << "Buffer size: " << buffer_size_mb + << " MB | Iterations: " << iterations << std::endl; + void *src_ptr, *dst_ptr; - if (!checkCudaErrorReturn(cudaSetDevice(src_gpu), "Failed to set source device for allocation")) { + if (!checkCudaErrorReturn(cudaSetDevice(src_gpu), + "Failed to set source device for allocation")) { return; } - - if (!checkCudaErrorReturn(cudaMalloc(&src_ptr, buffer_size_mb * 1024 * 1024), "Failed to allocate source memory")) { + + if (!checkCudaErrorReturn( + cudaMalloc(&src_ptr, buffer_size_mb * 1024 * 1024), + "Failed to allocate source memory")) { return; } - - if (!checkCudaErrorReturn(cudaSetDevice(dst_gpu), "Failed to set destination device for allocation")) { + + if (!checkCudaErrorReturn( + cudaSetDevice(dst_gpu), + "Failed to set destination device for allocation")) { cudaFree(src_ptr); return; } - - if (!checkCudaErrorReturn(cudaMalloc(&dst_ptr, buffer_size_mb * 1024 * 1024), "Failed to allocate destination memory")) { + + if (!checkCudaErrorReturn( + cudaMalloc(&dst_ptr, buffer_size_mb * 1024 * 1024), + "Failed to allocate destination memory")) { cudaFree(src_ptr); return; } - + // Initialize source buffer - if (!checkCudaErrorReturn(cudaSetDevice(src_gpu), "Failed to set source device for memset")) { + if (!checkCudaErrorReturn(cudaSetDevice(src_gpu), + "Failed to set source device for memset")) { cudaFree(src_ptr); cudaFree(dst_ptr); return; } - - if (!checkCudaErrorReturn(cudaMemset(src_ptr, 0x42, buffer_size_mb * 1024 * 1024), "Failed to initialize source buffer")) { + + if (!checkCudaErrorReturn( + cudaMemset(src_ptr, 0x42, buffer_size_mb * 1024 * 1024), + "Failed to initialize source buffer")) { cudaFree(src_ptr); cudaFree(dst_ptr); return; } - - if (!checkCudaErrorReturn(cudaDeviceSynchronize(), "Failed to synchronize device after memset")) { + + if (!checkCudaErrorReturn(cudaDeviceSynchronize(), + "Failed to synchronize device after memset")) { cudaFree(src_ptr); cudaFree(dst_ptr); return; } - + std::vector copy_times; - + for (int i = 0; i < iterations; i++) { - double copy_time = measureCopyTime(dst_ptr, src_ptr, buffer_size_mb * 1024 * 1024, - src_gpu, cudaMemcpyDeviceToDevice); - + double copy_time = + measureCopyTime(dst_ptr, src_ptr, buffer_size_mb * 1024 * 1024, + src_gpu, cudaMemcpyDeviceToDevice); + if (copy_time > 0) { copy_times.push_back(copy_time); - + if (i % 100 == 0 || i == iterations - 1) { - double bandwidth_gbps = (buffer_size_mb / 1024.0) / (copy_time / 1000.0); - std::cout << " Iteration " << (i + 1) << "/" << iterations - << " → " << std::fixed << std::setprecision(2) << bandwidth_gbps << " GB/s" << std::endl; + double bandwidth_gibps = + (buffer_size_mb / 1024.0) / (copy_time / 1000.0); + std::cout << " Iteration " << (i + 1) << "/" << iterations + << " → " << std::fixed << std::setprecision(2) + << bandwidth_gibps << " GiB/s" << std::endl; } } } - - - if (!copy_times.empty()) { - double avg_time = std::accumulate(copy_times.begin(), copy_times.end(), 0.0) / copy_times.size(); - double min_time = *std::min_element(copy_times.begin(), copy_times.end()); - double max_time = *std::max_element(copy_times.begin(), copy_times.end()); - - double avg_bandwidth_gbps = (buffer_size_mb / 1024.0) / (avg_time / 1000.0); - double min_bandwidth_gbps = (buffer_size_mb / 1024.0) / (max_time / 1000.0); - double max_bandwidth_gbps = (buffer_size_mb / 1024.0) / (min_time / 1000.0); - double avg_latency_ms = avg_time; - + + BandwidthStats stats = computeBandwidthStats(copy_times, buffer_size_mb); + if (stats.valid) { std::cout << std::endl; std::cout << "Performance Results:" << std::endl; - std::cout << " ├─ Average bandwidth: " << std::fixed << std::setprecision(2) << avg_bandwidth_gbps << " GB/s" << std::endl; - std::cout << " ├─ Min bandwidth: " << std::fixed << std::setprecision(2) << min_bandwidth_gbps << " GB/s" << std::endl; - std::cout << " ├─ Max bandwidth: " << std::fixed << std::setprecision(2) << max_bandwidth_gbps << " GB/s" << std::endl; - std::cout << " └─ Average latency: " << std::fixed << std::setprecision(3) << avg_latency_ms << " ms" << std::endl; + std::cout << " ├─ Average bandwidth: " << std::fixed + << std::setprecision(2) << stats.avgGiBps << " GiB/s" + << std::endl; + std::cout << " ├─ Min bandwidth: " << std::fixed + << std::setprecision(2) << stats.minGiBps << " GiB/s" + << std::endl; + std::cout << " ├─ Max bandwidth: " << std::fixed + << std::setprecision(2) << stats.maxGiBps << " GiB/s" + << std::endl; + std::cout << " └─ Average latency: " << std::fixed + << std::setprecision(3) << stats.avgLatencyMs << " ms" + << std::endl; } - + checkCudaErrorReturn(cudaFree(src_ptr), "Failed to free source memory"); - checkCudaErrorReturn(cudaFree(dst_ptr), "Failed to free destination memory"); + checkCudaErrorReturn(cudaFree(dst_ptr), + "Failed to free destination memory"); } - // Default parameter values -struct TestConfig { - int iterations = 100; - size_t buffer_size_mb = 1000; - int src_gpu_id = 0; - int dst_gpu_id = 1; - bool help = false; -}; - - // Print usage information +// Print usage information void printUsage(const char* program_name) { - std::cout << "Usage: " << program_name << " [OPTIONS]\n" - << "Options:\n" - << " -i, --iterations NUM Number of iterations (default: 100)\n" - << " -b, --buffer-size NUM Buffer size in MB (default: 1000)\n" - << " -s, --src-gpu NUM Source GPU ID (default: 0)\n" - << " -d, --dst-gpu NUM Destination GPU ID (default: 1)\n" - << " -h, --help Show this help message\n" - << "\n" - << "Example:\n" - << " " << program_name << " -i 200 -b 2000 -s 0 -d 1\n" - << std::endl; -} - - // Parse command line arguments -TestConfig parseCommandLine(int argc, char* argv[]) { - TestConfig config; - - static struct option long_options[] = { - {"iterations", required_argument, 0, 'i'}, - {"buffer-size", required_argument, 0, 'b'}, - {"src-gpu", required_argument, 0, 's'}, - {"dst-gpu", required_argument, 0, 'd'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - int opt; - int option_index = 0; - - while ((opt = getopt_long(argc, argv, "i:b:s:d:h", long_options, &option_index)) != -1) { - switch (opt) { - case 'i': - config.iterations = std::atoi(optarg); - if (config.iterations <= 0) { - std::cerr << "Error: iterations must be positive" << std::endl; - exit(1); - } - break; - case 'b': - config.buffer_size_mb = std::atoi(optarg); - if (config.buffer_size_mb <= 0) { - std::cerr << "Error: buffer size must be positive" << std::endl; - exit(1); - } - break; - case 's': - config.src_gpu_id = std::atoi(optarg); - if (config.src_gpu_id < 0) { - std::cerr << "Error: source GPU ID must be non-negative" << std::endl; - exit(1); - } - break; - case 'd': - config.dst_gpu_id = std::atoi(optarg); - if (config.dst_gpu_id < 0) { - std::cerr << "Error: destination GPU ID must be non-negative" << std::endl; - exit(1); - } - break; - case 'h': - config.help = true; - break; - case '?': - // getopt already printed an error message - exit(1); - default: - abort(); - } - } - - return config; + std::cout + << "Usage: " << program_name << " [OPTIONS]\n" + << "Options:\n" + << " -i, --iterations NUM Number of iterations (default: 100)\n" + << " -b, --buffer-size NUM Buffer size in MB (default: 1000)\n" + << " -s, --src-gpu NUM Source GPU ID (default: 0)\n" + << " -d, --dst-gpu NUM Destination GPU ID (default: 1)\n" + << " -h, --help Show this help message\n" + << "\n" + << "Example:\n" + << " " << program_name << " -i 200 -b 2000 -s 0 -d 1\n" + << std::endl; } int main(int argc, char* argv[]) { - // Parse command line arguments - TestConfig config = parseCommandLine(argc, argv); - + // Parse command-line arguments + TestConfig config = parseBwTestArgs(argc, argv); + if (config.help) { printUsage(argv[0]); return 0; } - - std::cout << "╔══════════════════════════════════════════════════════════════╗" << std::endl; - std::cout << "║ NVLink Performance Analysis ║" << std::endl; - std::cout << "╚══════════════════════════════════════════════════════════════╝" << std::endl; + if (!config.ok) { + std::cerr << "Error: " << config.errorMessage << std::endl; + printUsage(argv[0]); + return 1; + } + + std::cout + << "╔══════════════════════════════════════════════════════════════╗" + << std::endl; + std::cout + << "║ NVLink Performance Analysis ║" + << std::endl; + std::cout + << "╚══════════════════════════════════════════════════════════════╝" + << std::endl; std::cout << std::endl; std::cout << "Configuration:" << std::endl; std::cout << " • Iterations: " << config.iterations << std::endl; - std::cout << " • Buffer size: " << config.buffer_size_mb << " MB" << std::endl; + std::cout << " • Buffer size: " << config.buffer_size_mb << " MB" + << std::endl; std::cout << " • Source GPU: " << config.src_gpu_id << std::endl; std::cout << " • Destination GPU: " << config.dst_gpu_id << std::endl; std::cout << std::endl; - + int deviceCount; cudaError_t err = cudaGetDeviceCount(&deviceCount); if (!checkCudaErrorReturn(err, "Failed to get device count in main")) { return 1; } - + if (deviceCount < 2) { std::cout << "Need at least 2 GPUs for NVLink test" << std::endl; return 1; } - - // Validate GPU IDs + + // Validate GPU IDs if (config.src_gpu_id >= deviceCount || config.dst_gpu_id >= deviceCount) { - std::cerr << "Error: GPU ID out of range. Available GPUs: 0-" << (deviceCount - 1) << std::endl; + std::cerr << "Error: GPU ID out of range. Available GPUs: 0-" + << (deviceCount - 1) << std::endl; return 1; } - + if (config.src_gpu_id == config.dst_gpu_id) { - std::cerr << "Error: Source and destination GPU must be different" << std::endl; + std::cerr << "Error: Source and destination GPU must be different" + << std::endl; return 1; } - + int src_gpu = config.src_gpu_id; int dst_gpu = config.dst_gpu_id; - + printGPUInfo(src_gpu, dst_gpu); - + if (!checkP2PSupport(src_gpu, dst_gpu)) { std::cout << "P2P not supported" << std::endl; return 1; } - + if (!enableP2PAccess(src_gpu, dst_gpu)) { std::cout << "Failed to enable P2P access" << std::endl; return 1; } - + // Run test with configured buffer size size_t buffer_size = config.buffer_size_mb; - + // Test cudaMemcpyDeviceToDevice testCopyPerformance(src_gpu, dst_gpu, buffer_size, config.iterations); - + // Test completed successfully std::cout << std::endl; std::cout << "✓ NVLink bandwidth test completed successfully!" << std::endl; - + return 0; -} \ No newline at end of file +} \ No newline at end of file diff --git a/monitor/arg_parser.cpp b/monitor/arg_parser.cpp new file mode 100644 index 0000000..1870549 --- /dev/null +++ b/monitor/arg_parser.cpp @@ -0,0 +1,57 @@ +#include "arg_parser.h" + +#include + +MonitorCliArgs parseMonitorArgs(int argc, char* argv[]) { + MonitorCliArgs args; + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + + if (arg == "-h" || arg == "--help") { + args.helpRequested = true; + return args; + } else if (arg == "-v" || arg == "--verbose") { + args.verbose = true; + } else if (arg == "-i" || arg == "--interval") { + if (i + 1 >= argc) { + args.ok = false; + args.errorMessage = "Missing value for " + arg; + return args; + } + try { + args.interval = std::stod(argv[++i]); + if (args.interval <= 0) { + args.ok = false; + args.errorMessage = "Interval must be positive"; + return args; + } + } catch (const std::exception&) { + args.ok = false; + args.errorMessage = + std::string("Invalid interval value: ") + argv[i]; + return args; + } + } else if (arg == "-o" || arg == "--output") { + if (i + 1 >= argc) { + args.ok = false; + args.errorMessage = "Missing filename for " + arg; + return args; + } + args.outputFilename = argv[++i]; + } else if (arg == "-c" || arg == "--continuous") { + if (i + 1 < argc && (std::string(argv[i + 1]) == "true" || + std::string(argv[i + 1]) == "false")) { + args.continuous = (std::string(argv[++i]) == "true"); + } else { + args.continuous = true; // Default if no value specified + } + } else { + args.ok = false; + args.errorMessage = "Unknown option: " + arg; + return args; + } + } + + return args; +} diff --git a/monitor/arg_parser.h b/monitor/arg_parser.h new file mode 100644 index 0000000..f5a43ff --- /dev/null +++ b/monitor/arg_parser.h @@ -0,0 +1,21 @@ +#ifndef NVLINK_MONITOR_ARG_PARSER_H +#define NVLINK_MONITOR_ARG_PARSER_H + +#include + +// Parsed command-line arguments for nvlink_monitor. +struct MonitorCliArgs { + double interval = 1.0; + bool continuous = true; + bool verbose = false; + std::string outputFilename; + bool helpRequested = false; + bool ok = true; + std::string errorMessage; +}; + +// Parses nvlink_monitor command-line arguments. +// On error, sets ok=false and errorMessage (does not print or exit). +MonitorCliArgs parseMonitorArgs(int argc, char* argv[]); + +#endif // NVLINK_MONITOR_ARG_PARSER_H diff --git a/monitor/bandwidth_calc.cpp b/monitor/bandwidth_calc.cpp new file mode 100644 index 0000000..eb797f8 --- /dev/null +++ b/monitor/bandwidth_calc.cpp @@ -0,0 +1,75 @@ +#include "bandwidth_calc.h" + +#include + +std::vector calculateBandwidth( + const std::vector& snapshot1, + const std::vector& snapshot2, double timeDelta, + bool verbose) { + std::vector results; + + for (size_t i = 0; i < snapshot2.size(); i++) { + if (i >= snapshot1.size()) continue; + + const auto& s1 = snapshot1[i]; + const auto& s2 = snapshot2[i]; + + GPUMonitorResult result; + result.gpuId = s2.gpuId; + result.nvLinkCount = s2.nvLinkCount; + result.totalTxGiBps = 0.0; + result.totalRxGiBps = 0.0; + + for (size_t j = 0; j < s2.links.size(); j++) { + if (j >= s1.links.size()) continue; + + const auto& link1 = s1.links[j]; + const auto& link2 = s2.links[j]; + + // Calculate byte differences (NVML returns KiB) + long long txDelta = static_cast(link2.txBytes) - + static_cast(link1.txBytes); + long long rxDelta = static_cast(link2.rxBytes) - + static_cast(link1.rxBytes); + + // Handle overflow cases with detailed logging + if (txDelta < 0) { + if (verbose) { + std::cerr << "Warning: TX counter overflow detected on GPU " + << s2.gpuId << " Link " << link2.linkId + << std::endl; + } + txDelta = 0; + } + if (rxDelta < 0) { + if (verbose) { + std::cerr << "Warning: RX counter overflow detected on GPU " + << s2.gpuId << " Link " << link2.linkId + << std::endl; + } + rxDelta = 0; + } + + // Convert KiB directly to GiB/s (NVML counters are in KiB) + double txRate = + static_cast(txDelta) / (timeDelta * 1024.0 * 1024.0); + double rxRate = + static_cast(rxDelta) / (timeDelta * 1024.0 * 1024.0); + + NvLinkData linkData; + linkData.linkId = link2.linkId; + linkData.txGiBps = txRate; + linkData.rxGiBps = rxRate; + linkData.txBytes = link2.txBytes; + linkData.rxBytes = link2.rxBytes; + + result.links.push_back(linkData); + result.totalTxGiBps += txRate; + result.totalRxGiBps += rxRate; + } + + results.push_back(result); + } + + return results; +} diff --git a/monitor/bandwidth_calc.h b/monitor/bandwidth_calc.h new file mode 100644 index 0000000..39c5846 --- /dev/null +++ b/monitor/bandwidth_calc.h @@ -0,0 +1,18 @@ +#ifndef NVLINK_MONITOR_BANDWIDTH_CALC_H +#define NVLINK_MONITOR_BANDWIDTH_CALC_H + +#include + +#include "nvlink_monitor.h" + +// Calculates bandwidth between two NVML counter snapshots. +// +// Counters are in KiB; results are stored in the txGiBps/rxGiBps fields +// as GiB/s. Negative deltas from counter overflow are clamped to 0, +// with an optional stderr warning when verbose == true. +std::vector calculateBandwidth( + const std::vector& snapshot1, + const std::vector& snapshot2, double timeDelta, + bool verbose); + +#endif // NVLINK_MONITOR_BANDWIDTH_CALC_H diff --git a/monitor/nvlink_monitor.cpp b/monitor/nvlink_monitor.cpp index f09dfeb..e1c174d 100644 --- a/monitor/nvlink_monitor.cpp +++ b/monitor/nvlink_monitor.cpp @@ -1,8 +1,12 @@ #include "nvlink_monitor.h" -#include #include +#include + +#include "arg_parser.h" +#include "bandwidth_calc.h" + // Global flag for signal handling volatile bool g_running = true; @@ -113,8 +117,8 @@ std::vector NvLinkMonitor::getNvLinkData() { GPUMonitorResult result; result.gpuId = gpu.id; result.nvLinkCount = gpu.nvLinkCount; - result.totalTxGBps = 0.0; - result.totalRxGBps = 0.0; + result.totalTxGiBps = 0.0; + result.totalRxGiBps = 0.0; // Get utilization counters for each NvLink using Field Values for (unsigned int link = 0; link < gpu.nvLinkCount; link++) { @@ -126,16 +130,14 @@ std::vector NvLinkMonitor::getNvLinkData() { linkData.linkId = link; linkData.rxBytes = 0; linkData.txBytes = 0; - linkData.rxGBps = 0.0; - linkData.txGBps = 0.0; + linkData.rxGiBps = 0.0; + linkData.txGiBps = 0.0; // Try Field Values API first nvmlFieldValue_t fieldValues[2]; - fieldValues[0].fieldId = - NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_TX; + fieldValues[0].fieldId = NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_TX; fieldValues[0].scopeId = link; - fieldValues[1].fieldId = - NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_RX; + fieldValues[1].fieldId = NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_RX; fieldValues[1].scopeId = link; nvmlReturn_t fieldResult = @@ -162,10 +164,9 @@ std::vector NvLinkMonitor::getNvLinkData() { NVML_SUCCESS) { linkData.rxBytes = rxCounter; linkData.txBytes = txCounter; - std::cout - << " Link " << link - << " (Traditional): TX=" << linkData.txBytes - << " RX=" << linkData.rxBytes << std::endl; + std::cout << " Link " << link + << " (Traditional): TX=" << linkData.txBytes + << " RX=" << linkData.rxBytes << std::endl; } else { std::cerr << "Failed to get utilization counters for GPU " @@ -191,71 +192,7 @@ std::ostream& NvLinkMonitor::getOutputStream() { std::vector NvLinkMonitor::calculateBandwidth( const std::vector& snapshot1, const std::vector& snapshot2, double timeDelta) { - std::vector results; - - for (size_t i = 0; i < snapshot2.size(); i++) { - if (i >= snapshot1.size()) continue; - - const auto& s1 = snapshot1[i]; - const auto& s2 = snapshot2[i]; - - GPUMonitorResult result; - result.gpuId = s2.gpuId; - result.nvLinkCount = s2.nvLinkCount; - result.totalTxGBps = 0.0; - result.totalRxGBps = 0.0; - - for (size_t j = 0; j < s2.links.size(); j++) { - if (j >= s1.links.size()) continue; - - const auto& link1 = s1.links[j]; - const auto& link2 = s2.links[j]; - - // Calculate byte differences (NVML returns KiB, convert to bytes) - long long txDelta = static_cast(link2.txBytes) - - static_cast(link1.txBytes); - long long rxDelta = static_cast(link2.rxBytes) - - static_cast(link1.rxBytes); - - // Handle overflow cases with detailed logging - if (txDelta < 0) { - if (verboseOutput) { - std::cerr << "Warning: TX counter overflow detected on GPU " - << s2.gpuId << " Link " << link2.linkId << std::endl; - } - txDelta = 0; - } - if (rxDelta < 0) { - if (verboseOutput) { - std::cerr << "Warning: RX counter overflow detected on GPU " - << s2.gpuId << " Link " << link2.linkId << std::endl; - } - rxDelta = 0; - } - - // Convert KiB directly to GiB/s - // NVML returns KiB, convert directly to GiB/s - double txGiBps = static_cast(txDelta) / - (timeDelta * 1024.0 * 1024.0); - double rxGiBps = static_cast(rxDelta) / - (timeDelta * 1024.0 * 1024.0); - - NvLinkData linkData; - linkData.linkId = link2.linkId; - linkData.txGBps = txGiBps; - linkData.rxGBps = rxGiBps; - linkData.txBytes = link2.txBytes; - linkData.rxBytes = link2.rxBytes; - - result.links.push_back(linkData); - result.totalTxGBps += txGiBps; - result.totalRxGBps += rxGiBps; - } - - results.push_back(result); - } - - return results; + return ::calculateBandwidth(snapshot1, snapshot2, timeDelta, verboseOutput); } void NvLinkMonitor::formatGPUResult( @@ -274,8 +211,8 @@ void NvLinkMonitor::formatGPUResult( getOutputStream() << "GPU " << gpu.gpuId << " (" << gpu.nvLinkCount << " links) " << "RX: " << std::fixed << std::setprecision(1) - << std::setw(4) << gpu.totalRxGBps - << " GiB/s, TX: " << std::setw(4) << gpu.totalTxGBps + << std::setw(4) << gpu.totalRxGiBps + << " GiB/s, TX: " << std::setw(4) << gpu.totalTxGiBps << " GiB/s" << std::endl; } } @@ -296,8 +233,8 @@ void NvLinkMonitor::formatDetailedGPUResult( getOutputStream() << "GPU " << gpu.gpuId << " (" << gpu.nvLinkCount << " links) " << "Total RX: " << std::fixed << std::setprecision(1) - << std::setw(4) << gpu.totalRxGBps - << " GiB/s, TX: " << std::setw(4) << gpu.totalTxGBps + << std::setw(4) << gpu.totalRxGiBps + << " GiB/s, TX: " << std::setw(4) << gpu.totalTxGiBps << " GiB/s" << std::endl; // Print individual link details @@ -305,8 +242,8 @@ void NvLinkMonitor::formatDetailedGPUResult( getOutputStream() << " Link " << std::setw(2) << link.linkId << " RX: " << std::fixed << std::setprecision(1) << std::setw(6) - << link.rxGBps << " GiB/s, TX: " << std::setw(6) << link.txGBps - << " GiB/s" << std::endl; + << link.rxGiBps << " GiB/s, TX: " << std::setw(6) + << link.txGiBps << " GiB/s" << std::endl; } getOutputStream() << std::endl; // Add blank line between GPUs } @@ -316,24 +253,25 @@ void NvLinkMonitor::runContinuousMonitoring(double interval) { std::cout << "Starting continuous monitoring, interval: " << interval << "s" << std::endl; std::cout << "Press Ctrl+C to stop monitoring" << std::endl; - - // Set high priority for more accurate timing - #ifdef _GNU_SOURCE + +// Set high priority for more accurate timing +#ifdef _GNU_SOURCE // Try to set real-time priority for better timing accuracy struct sched_param param; param.sched_priority = sched_get_priority_max(SCHED_FIFO); if (sched_setscheduler(0, SCHED_FIFO, ¶m) == 0) { - std::cout << "Set real-time scheduling priority for improved accuracy" << std::endl; + std::cout << "Set real-time scheduling priority for improved accuracy" + << std::endl; } - #endif +#endif auto lastSnapshot = getNvLinkData(); auto lastTime = std::chrono::high_resolution_clock::now(); while (g_running) { // Use microsecond precision for sleep to improve timing accuracy - std::this_thread::sleep_for( - std::chrono::microseconds(static_cast(interval * 1000000))); + std::this_thread::sleep_for(std::chrono::microseconds( + static_cast(interval * 1000000))); if (!g_running) break; @@ -344,14 +282,17 @@ void NvLinkMonitor::runContinuousMonitoring(double interval) { auto timeDiff = std::chrono::duration_cast( currentTime - lastTime); double actualInterval = - timeDiff.count() / 1000000000.0; // Convert to seconds with nanosecond precision + timeDiff.count() / + 1000000000.0; // Convert to seconds with nanosecond precision - // Check for minimum time interval to avoid division by very small numbers - const double MIN_INTERVAL = 0.000001; // 1 microsecond minimum + // Check for minimum time interval to avoid division by very small + // numbers + const double MIN_INTERVAL = 0.000001; // 1 microsecond minimum if (actualInterval < MIN_INTERVAL) { if (verboseOutput) { - std::cerr << "Warning: Time interval too small (" << actualInterval - << "s), using minimum interval" << std::endl; + std::cerr << "Warning: Time interval too small (" + << actualInterval << "s), using minimum interval" + << std::endl; } actualInterval = MIN_INTERVAL; } @@ -362,8 +303,9 @@ void NvLinkMonitor::runContinuousMonitoring(double interval) { // Add timing precision information in verbose mode if (verboseOutput) { formatDetailedGPUResult(results); - getOutputStream() << " [Timing: " << std::fixed << std::setprecision(6) - << actualInterval << "s]" << std::endl; + getOutputStream() + << " [Timing: " << std::fixed << std::setprecision(6) + << actualInterval << "s]" << std::endl; } else { formatGPUResult(results); } @@ -436,68 +378,16 @@ void printHelp(const char* programName) { } int main(int argc, char* argv[]) { - // Parse command line arguments - double interval = 1.0; - bool continuous = true; // Default to continuous mode - bool verbose = false; - std::string outputFilename = ""; // Output file name - - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - - // Help options - if (arg == "-h" || arg == "--help") { - printHelp(argv[0]); - return 0; - } - // Verbose options - else if (arg == "-v" || arg == "--verbose") { - verbose = true; - } - // Interval options - else if (arg == "-i" || arg == "--interval") { - if (i + 1 >= argc) { - std::cerr << "Error: Missing value for " << arg << std::endl; - printHelp(argv[0]); - return 1; - } - try { - interval = std::stod(argv[++i]); - if (interval <= 0) { - std::cerr << "Error: Interval must be positive" - << std::endl; - return 1; - } - } catch (const std::exception& e) { - std::cerr << "Error: Invalid interval value: " << argv[i] - << std::endl; - return 1; - } - } - // Output file options - else if (arg == "-o" || arg == "--output") { - if (i + 1 >= argc) { - std::cerr << "Error: Missing filename for " << arg << std::endl; - printHelp(argv[0]); - return 1; - } - outputFilename = argv[++i]; - } - // Continuous options - else if (arg == "-c" || arg == "--continuous") { - if (i + 1 < argc && (std::string(argv[i + 1]) == "true" || - std::string(argv[i + 1]) == "false")) { - continuous = (std::string(argv[++i]) == "true"); - } else { - continuous = true; // Default to true if no value specified - } - } - // Unknown option - else { - std::cerr << "Error: Unknown option: " << arg << std::endl; - printHelp(argv[0]); - return 1; - } + MonitorCliArgs args = parseMonitorArgs(argc, argv); + + if (args.helpRequested) { + printHelp(argv[0]); + return 0; + } + if (!args.ok) { + std::cerr << "Error: " << args.errorMessage << std::endl; + printHelp(argv[0]); + return 1; } // Setup signal handling @@ -505,12 +395,12 @@ int main(int argc, char* argv[]) { signal(SIGTERM, signal_handler); try { - NvLinkMonitor monitor(verbose, outputFilename); + NvLinkMonitor monitor(args.verbose, args.outputFilename); - if (continuous) { - monitor.runContinuousMonitoring(interval); + if (args.continuous) { + monitor.runContinuousMonitoring(args.interval); } else { - monitor.runSingleMonitoring(interval); + monitor.runSingleMonitoring(args.interval); } } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; diff --git a/monitor/nvlink_monitor.h b/monitor/nvlink_monitor.h index de3050c..802d32a 100644 --- a/monitor/nvlink_monitor.h +++ b/monitor/nvlink_monitor.h @@ -1,15 +1,16 @@ #ifndef NVLINK_MONITOR_H #define NVLINK_MONITOR_H +#include +#include + +#include +#include +#include #include -#include #include -#include #include -#include -#include -#include -#include +#include // Global flag for signal handling extern volatile bool g_running; @@ -21,7 +22,7 @@ void signal_handler(int signal); struct GPUData { std::string id; std::string uuid; - nvmlDevice_t device; // Single device handle + nvmlDevice_t device; // Single device handle unsigned int nvLinkCount; // Number of active NvLinks }; @@ -30,22 +31,22 @@ struct NvLinkData { unsigned int linkId; unsigned long long txBytes; unsigned long long rxBytes; - double txGBps; - double rxGBps; + double txGiBps; + double rxGiBps; }; // Structure to hold GPU monitoring result struct GPUMonitorResult { std::string gpuId; unsigned int nvLinkCount; // Number of active NvLinks - double totalTxGBps; - double totalRxGBps; + double totalTxGiBps; + double totalRxGiBps; std::vector links; }; /** * @brief NvLink Monitor class for monitoring NVIDIA NVLink bandwidth and status - * + * * This class provides functionality to: * - Discover and initialize GPUs * - Monitor NvLink utilization @@ -53,38 +54,40 @@ struct GPUMonitorResult { * - Run continuous or single monitoring sessions */ class NvLinkMonitor { -private: + private: std::vector gpus; std::vector lastResults; - bool verboseOutput; // Flag for detailed NvLink output + bool verboseOutput; // Flag for detailed NvLink output std::ofstream outputFile; // Output file stream - bool fileOutput; // Flag for file output - -public: + bool fileOutput; // Flag for file output + + public: /** * @brief Constructor - initializes NVML and discovers GPUs * @param verbose Enable detailed NvLink output - * @param outputFilename Optional output file name (empty for console output) - * @throws std::runtime_error if NVML initialization fails or file cannot be opened + * @param outputFilename Optional output file name (empty for console + * output) + * @throws std::runtime_error if NVML initialization fails or file cannot be + * opened */ NvLinkMonitor(bool verbose = false, const std::string& outputFilename = ""); - + /** * @brief Destructor - shuts down NVML */ ~NvLinkMonitor(); - + /** * @brief Discovers available GPUs and their NvLink capabilities */ void discoverGPUs(); - + /** * @brief Gets current NvLink data from all GPUs * @return Vector of GPU monitoring results */ std::vector getNvLinkData(); - + /** * @brief Calculates bandwidth between two snapshots * @param snapshot1 First snapshot @@ -94,34 +97,32 @@ class NvLinkMonitor { */ std::vector calculateBandwidth( const std::vector& snapshot1, - const std::vector& snapshot2, - double timeDelta - ); - + const std::vector& snapshot2, double timeDelta); + /** * @brief Formats and prints GPU monitoring results * @param results Vector of GPU monitoring results to display */ void formatGPUResult(const std::vector& results); - + /** * @brief Formats and prints detailed NvLink information * @param results Vector of GPU monitoring results to display */ void formatDetailedGPUResult(const std::vector& results); - + /** * @brief Gets the output stream (file or console) * @return Reference to the output stream */ std::ostream& getOutputStream(); - + /** * @brief Runs continuous monitoring with specified interval * @param interval Monitoring interval in seconds */ void runContinuousMonitoring(double interval); - + /** * @brief Runs single monitoring session * @param interval Interval between snapshots in seconds @@ -129,4 +130,4 @@ class NvLinkMonitor { void runSingleMonitoring(double interval); }; -#endif // NVLINK_MONITOR_H \ No newline at end of file +#endif // NVLINK_MONITOR_H \ No newline at end of file diff --git a/test/test_arg_parser.cpp b/test/test_arg_parser.cpp new file mode 100644 index 0000000..56ab787 --- /dev/null +++ b/test/test_arg_parser.cpp @@ -0,0 +1,168 @@ +#include + +#include +#include +#include + +#include "monitor/arg_parser.h" + +namespace { + +// Builds a mutable argv from string literals for parseMonitorArgs. +struct Argv { + std::vector> buffers; + std::vector ptrs; + explicit Argv(std::initializer_list args) { + buffers.reserve(args.size()); + for (const char* s : args) { + buffers.emplace_back(s, s + std::strlen(s) + 1); + } + ptrs.reserve(args.size() + 1); + for (auto& b : buffers) { + ptrs.push_back(b.data()); + } + ptrs.push_back(nullptr); + } + int argc() const { return static_cast(ptrs.size()) - 1; } + char** argv() { return ptrs.data(); } +}; + +} // namespace + +TEST(MonitorArgs, Defaults) { + Argv a{"nvlink_monitor"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_DOUBLE_EQ(args.interval, 1.0); + EXPECT_TRUE(args.continuous); + EXPECT_FALSE(args.verbose); + EXPECT_TRUE(args.outputFilename.empty()); + EXPECT_FALSE(args.helpRequested); +} + +TEST(MonitorArgs, HelpShort) { + Argv a{"nvlink_monitor", "-h"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.helpRequested); + EXPECT_TRUE(args.ok); +} + +TEST(MonitorArgs, HelpLong) { + Argv a{"nvlink_monitor", "--help"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.helpRequested); +} + +TEST(MonitorArgs, VerboseShort) { + Argv a{"nvlink_monitor", "-v"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.verbose); +} + +TEST(MonitorArgs, VerboseLong) { + Argv a{"nvlink_monitor", "--verbose"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.verbose); +} + +TEST(MonitorArgs, IntervalValue) { + Argv a{"nvlink_monitor", "-i", "0.5"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_DOUBLE_EQ(args.interval, 0.5); +} + +TEST(MonitorArgs, IntervalLong) { + Argv a{"nvlink_monitor", "--interval", "2.0"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_DOUBLE_EQ(args.interval, 2.0); +} + +TEST(MonitorArgs, IntervalZeroRejected) { + Argv a{"nvlink_monitor", "-i", "0"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_FALSE(args.ok); + EXPECT_FALSE(args.errorMessage.empty()); +} + +TEST(MonitorArgs, IntervalNegativeRejected) { + Argv a{"nvlink_monitor", "-i", "-1"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_FALSE(args.ok); +} + +TEST(MonitorArgs, IntervalNonNumericRejected) { + Argv a{"nvlink_monitor", "-i", "abc"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_FALSE(args.ok); +} + +TEST(MonitorArgs, IntervalMissingValue) { + Argv a{"nvlink_monitor", "-i"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_FALSE(args.ok); +} + +TEST(MonitorArgs, OutputFile) { + Argv a{"nvlink_monitor", "-o", "out.log"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_EQ(args.outputFilename, "out.log"); +} + +TEST(MonitorArgs, OutputLong) { + Argv a{"nvlink_monitor", "--output", "out.log"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_EQ(args.outputFilename, "out.log"); +} + +TEST(MonitorArgs, OutputMissingFilename) { + Argv a{"nvlink_monitor", "-o"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_FALSE(args.ok); +} + +TEST(MonitorArgs, ContinuousNoValueDefaultsTrue) { + Argv a{"nvlink_monitor", "-c"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_TRUE(args.continuous); +} + +TEST(MonitorArgs, ContinuousLongNoValue) { + Argv a{"nvlink_monitor", "--continuous"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_TRUE(args.continuous); +} + +TEST(MonitorArgs, ContinuousFalse) { + Argv a{"nvlink_monitor", "--continuous", "false"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_FALSE(args.continuous); +} + +TEST(MonitorArgs, ContinuousTrue) { + Argv a{"nvlink_monitor", "-c", "true"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_TRUE(args.continuous); +} + +TEST(MonitorArgs, UnknownOptionRejected) { + Argv a{"nvlink_monitor", "--bogus"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_FALSE(args.ok); +} + +TEST(MonitorArgs, CombinedOptions) { + Argv a{"nvlink_monitor", "-v", "-i", "0.5", "-o", "out.log"}; + auto args = parseMonitorArgs(a.argc(), a.argv()); + EXPECT_TRUE(args.ok); + EXPECT_TRUE(args.verbose); + EXPECT_DOUBLE_EQ(args.interval, 0.5); + EXPECT_EQ(args.outputFilename, "out.log"); +} diff --git a/test/test_bandwidth_calc.cpp b/test/test_bandwidth_calc.cpp new file mode 100644 index 0000000..8c7d92a --- /dev/null +++ b/test/test_bandwidth_calc.cpp @@ -0,0 +1,125 @@ +#include + +#include "monitor/bandwidth_calc.h" + +namespace { + +NvLinkData makeLink(unsigned int id, unsigned long long tx, + unsigned long long rx) { + NvLinkData l; + l.linkId = id; + l.txBytes = tx; + l.rxBytes = rx; + l.txGiBps = 0.0; + l.rxGiBps = 0.0; + return l; +} + +GPUMonitorResult makeGpu(const std::string& id, unsigned int linkCount, + const std::vector& links) { + GPUMonitorResult r; + r.gpuId = id; + r.nvLinkCount = linkCount; + r.totalTxGiBps = 0.0; + r.totalRxGiBps = 0.0; + r.links = links; + return r; +} + +} // namespace + +// 1048576 KiB = 1 GiB. Over 1 second -> 1.0 GiB/s. +TEST(CalculateBandwidth, PositiveDelta) { + auto s1 = makeGpu("0", 1, {makeLink(0, 0, 0)}); + auto s2 = makeGpu("0", 1, {makeLink(0, 1048576ULL, 0)}); + auto r = calculateBandwidth({s1}, {s2}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_NEAR(r[0].links[0].txGiBps, 1.0, 1e-9); + EXPECT_NEAR(r[0].links[0].rxGiBps, 0.0, 1e-9); + EXPECT_NEAR(r[0].totalTxGiBps, 1.0, 1e-9); + EXPECT_NEAR(r[0].totalRxGiBps, 0.0, 1e-9); +} + +TEST(CalculateBandwidth, ZeroDelta) { + auto s1 = makeGpu("0", 1, {makeLink(0, 500, 500)}); + auto s2 = makeGpu("0", 1, {makeLink(0, 500, 500)}); + auto r = calculateBandwidth({s1}, {s2}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_NEAR(r[0].links[0].txGiBps, 0.0, 1e-9); + EXPECT_NEAR(r[0].links[0].rxGiBps, 0.0, 1e-9); + EXPECT_NEAR(r[0].totalTxGiBps, 0.0, 1e-9); +} + +TEST(CalculateBandwidth, CounterOverflowClampedToZero) { + // s2 < s1 simulates counter overflow / wraparound. + auto s1 = makeGpu("0", 1, {makeLink(0, 1000000, 1000000)}); + auto s2 = makeGpu("0", 1, {makeLink(0, 100, 100)}); + auto r = calculateBandwidth({s1}, {s2}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_NEAR(r[0].links[0].txGiBps, 0.0, 1e-9); + EXPECT_NEAR(r[0].links[0].rxGiBps, 0.0, 1e-9); + EXPECT_NEAR(r[0].totalTxGiBps, 0.0, 1e-9); +} + +TEST(CalculateBandwidth, MultiLinkAggregation) { + auto s1 = makeGpu("0", 2, {makeLink(0, 0, 0), makeLink(1, 0, 0)}); + auto s2 = makeGpu("0", 2, + {makeLink(0, 1048576ULL, 0), makeLink(1, 0, 1048576ULL)}); + auto r = calculateBandwidth({s1}, {s2}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_NEAR(r[0].links[0].txGiBps, 1.0, 1e-9); + EXPECT_NEAR(r[0].links[1].rxGiBps, 1.0, 1e-9); + EXPECT_NEAR(r[0].totalTxGiBps, 1.0, 1e-9); + EXPECT_NEAR(r[0].totalRxGiBps, 1.0, 1e-9); +} + +TEST(CalculateBandwidth, MismatchedGpuCountDropsExtra) { + auto s1 = makeGpu("0", 1, {makeLink(0, 0, 0)}); + auto s2a = makeGpu("0", 1, {makeLink(0, 1048576ULL, 0)}); + auto s2b = makeGpu("1", 1, {makeLink(0, 1048576ULL, 0)}); + // s2 has 2 GPUs, s1 has 1 -> second GPU dropped. + auto r = calculateBandwidth({s1}, {s2a, s2b}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_EQ(r[0].gpuId, "0"); +} + +TEST(CalculateBandwidth, MismatchedLinkCountDropsExtra) { + auto s1 = makeGpu("0", 1, {makeLink(0, 0, 0)}); + auto s2 = makeGpu("0", 2, + {makeLink(0, 1048576ULL, 0), makeLink(1, 1048576ULL, 0)}); + // s2 has 2 links, s1 has 1 -> second link dropped. + auto r = calculateBandwidth({s1}, {s2}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + ASSERT_EQ(r[0].links.size(), 1u); + EXPECT_NEAR(r[0].totalTxGiBps, 1.0, 1e-9); +} + +TEST(CalculateBandwidth, MultiGpuTotals) { + auto s1a = makeGpu("0", 1, {makeLink(0, 0, 0)}); + auto s1b = makeGpu("1", 1, {makeLink(0, 0, 0)}); + auto s2a = makeGpu("0", 1, {makeLink(0, 1048576ULL, 0)}); + auto s2b = makeGpu("1", 1, {makeLink(0, 1048576ULL, 0)}); + auto r = calculateBandwidth({s1a, s1b}, {s2a, s2b}, 1.0, false); + ASSERT_EQ(r.size(), 2u); + EXPECT_NEAR(r[0].totalTxGiBps, 1.0, 1e-9); + EXPECT_NEAR(r[1].totalTxGiBps, 1.0, 1e-9); +} + +TEST(CalculateBandwidth, TimeDeltaAffectsRate) { + // Same delta, half the time -> double the rate. + auto s1 = makeGpu("0", 1, {makeLink(0, 0, 0)}); + auto s2 = makeGpu("0", 1, {makeLink(0, 1048576ULL, 0)}); + auto r = calculateBandwidth({s1}, {s2}, 0.5, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_NEAR(r[0].links[0].txGiBps, 2.0, 1e-9); +} + +TEST(CalculateBandwidth, PreservesSnapshotBytes) { + // Result should carry forward the s2 byte counters. + auto s1 = makeGpu("0", 1, {makeLink(0, 100, 200)}); + auto s2 = makeGpu("0", 1, {makeLink(0, 1048676ULL, 1048776ULL)}); + auto r = calculateBandwidth({s1}, {s2}, 1.0, false); + ASSERT_EQ(r.size(), 1u); + EXPECT_EQ(r[0].links[0].txBytes, 1048676ULL); + EXPECT_EQ(r[0].links[0].rxBytes, 1048776ULL); +} diff --git a/test/test_bw_stats.cpp b/test/test_bw_stats.cpp new file mode 100644 index 0000000..79bff3a --- /dev/null +++ b/test/test_bw_stats.cpp @@ -0,0 +1,54 @@ +#include + +#include + +#include "example/bw_stats.h" + +TEST(BandwidthStats, KnownValues) { + // bufferSizeMb=1024 -> 1.0 GiB. Times: {10,20,40} ms. + // avg=23.333ms -> avg_bw = 1.0/0.023333 = 42.857 GiB/s + // min_time=10ms -> max_bw = 1.0/0.01 = 100.0 + // max_time=40ms -> min_bw = 1.0/0.04 = 25.0 + auto s = computeBandwidthStats({10.0, 20.0, 40.0}, 1024); + EXPECT_TRUE(s.valid); + EXPECT_NEAR(s.avgGiBps, 42.857, 0.01); + EXPECT_NEAR(s.minGiBps, 25.0, 1e-9); + EXPECT_NEAR(s.maxGiBps, 100.0, 1e-9); + EXPECT_NEAR(s.avgLatencyMs, 23.3333, 0.001); +} + +TEST(BandwidthStats, SingleIteration) { + // bufferSizeMb=1000 -> 0.9765625 GiB. Time=5ms. + // bw = 0.9765625 / 0.005 = 195.3125 GiB/s + auto s = computeBandwidthStats({5.0}, 1000); + EXPECT_TRUE(s.valid); + EXPECT_NEAR(s.avgGiBps, 195.3125, 1e-9); + EXPECT_NEAR(s.minGiBps, 195.3125, 1e-9); + EXPECT_NEAR(s.maxGiBps, 195.3125, 1e-9); + EXPECT_NEAR(s.avgLatencyMs, 5.0, 1e-9); +} + +TEST(BandwidthStats, EmptyInput) { + auto s = computeBandwidthStats({}, 1024); + EXPECT_FALSE(s.valid); +} + +TEST(BandwidthStats, AllEqualTimings) { + // 1.0 GiB / 0.01s = 100.0 GiB/s for all three. + auto s = computeBandwidthStats({10.0, 10.0, 10.0}, 1024); + EXPECT_TRUE(s.valid); + EXPECT_NEAR(s.avgGiBps, 100.0, 1e-9); + EXPECT_NEAR(s.minGiBps, 100.0, 1e-9); + EXPECT_NEAR(s.maxGiBps, 100.0, 1e-9); + EXPECT_NEAR(s.avgLatencyMs, 10.0, 1e-9); +} + +TEST(BandwidthStats, MinMaxInversion) { + // Slowest copy (max time) -> min bandwidth; fastest -> max bandwidth. + auto s = computeBandwidthStats({5.0, 20.0}, 1024); + EXPECT_TRUE(s.valid); + // max_time=20ms -> min_bw = 1.0/0.02 = 50.0 + // min_time=5ms -> max_bw = 1.0/0.005 = 200.0 + EXPECT_NEAR(s.minGiBps, 50.0, 1e-9); + EXPECT_NEAR(s.maxGiBps, 200.0, 1e-9); +} diff --git a/test/test_bw_test_args.cpp b/test/test_bw_test_args.cpp new file mode 100644 index 0000000..36ead3e --- /dev/null +++ b/test/test_bw_test_args.cpp @@ -0,0 +1,176 @@ +#include + +#include +#include +#include + +#include "example/arg_parser.h" + +namespace { + +// Builds a mutable argv from string literals for parseBwTestArgs. +struct Argv { + std::vector> buffers; + std::vector ptrs; + explicit Argv(std::initializer_list args) { + buffers.reserve(args.size()); + for (const char* s : args) { + buffers.emplace_back(s, s + std::strlen(s) + 1); + } + ptrs.reserve(args.size() + 1); + for (auto& b : buffers) { + ptrs.push_back(b.data()); + } + ptrs.push_back(nullptr); + } + int argc() const { return static_cast(ptrs.size()) - 1; } + char** argv() { return ptrs.data(); } +}; + +} // namespace + +TEST(BwTestArgs, Defaults) { + Argv a{"nvlink_bw_test"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.iterations, 100); + EXPECT_EQ(c.buffer_size_mb, 1000u); + EXPECT_EQ(c.src_gpu_id, 0); + EXPECT_EQ(c.dst_gpu_id, 1); + EXPECT_FALSE(c.help); +} + +TEST(BwTestArgs, HelpShort) { + Argv a{"nvlink_bw_test", "-h"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_TRUE(c.help); +} + +TEST(BwTestArgs, HelpLong) { + Argv a{"nvlink_bw_test", "--help"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_TRUE(c.help); +} + +TEST(BwTestArgs, IterationsValid) { + Argv a{"nvlink_bw_test", "-i", "200"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.iterations, 200); +} + +TEST(BwTestArgs, IterationsZeroRejected) { + Argv a{"nvlink_bw_test", "-i", "0"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, IterationsNegativeRejected) { + Argv a{"nvlink_bw_test", "-i", "-1"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, IterationsNonNumericRejected) { + Argv a{"nvlink_bw_test", "-i", "abc"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, BufferSizeValid) { + Argv a{"nvlink_bw_test", "-b", "2000"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.buffer_size_mb, 2000u); +} + +TEST(BwTestArgs, BufferSizeNegativeOneRejected) { + // Regression: previously -1 wrapped size_t to SIZE_MAX and passed the + // <= 0 check (unsigned), then attempted an absurd cudaMalloc. + Argv a{"nvlink_bw_test", "-b", "-1"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, BufferSizeZeroRejected) { + Argv a{"nvlink_bw_test", "-b", "0"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, BufferSizeNonNumericRejected) { + Argv a{"nvlink_bw_test", "-b", "abc"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, SrcGpuValid) { + Argv a{"nvlink_bw_test", "-s", "2"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.src_gpu_id, 2); +} + +TEST(BwTestArgs, SrcGpuNegativeRejected) { + Argv a{"nvlink_bw_test", "-s", "-1"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, DstGpuValid) { + Argv a{"nvlink_bw_test", "-d", "3"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.dst_gpu_id, 3); +} + +TEST(BwTestArgs, DstGpuNegativeRejected) { + Argv a{"nvlink_bw_test", "-d", "-1"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, CombinedOptions) { + Argv a{"nvlink_bw_test", "-i", "200", "-b", "2000", "-s", "0", "-d", "1"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.iterations, 200); + EXPECT_EQ(c.buffer_size_mb, 2000u); + EXPECT_EQ(c.src_gpu_id, 0); + EXPECT_EQ(c.dst_gpu_id, 1); +} + +TEST(BwTestArgs, UnknownOptionRejected) { + Argv a{"nvlink_bw_test", "--bogus"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_FALSE(c.ok); + EXPECT_FALSE(c.errorMessage.empty()); +} + +TEST(BwTestArgs, LongOptions) { + Argv a{"nvlink_bw_test", + "--iterations", + "50", + "--buffer-size", + "500", + "--src-gpu", + "0", + "--dst-gpu", + "2"}; + auto c = parseBwTestArgs(a.argc(), a.argv()); + EXPECT_TRUE(c.ok); + EXPECT_EQ(c.iterations, 50); + EXPECT_EQ(c.buffer_size_mb, 500u); + EXPECT_EQ(c.src_gpu_id, 0); + EXPECT_EQ(c.dst_gpu_id, 2); +}