Initial commit
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
//! Extended Kernel Profiler
|
||||
//!
|
||||
//! Provides advanced kernel profiling capabilities for RustyTorch.
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{Level, debug, info, instrument, span};
|
||||
|
||||
use crate::error::{IntegrationError, Result};
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
use gpu_profiler::metal::{MetalProfiler, ProfilingReport, ProfilingSession};
|
||||
|
||||
/// Report format for profiling results
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReportFormat {
|
||||
/// Plain text format
|
||||
Text,
|
||||
/// JSON format
|
||||
Json,
|
||||
/// CSV format
|
||||
Csv,
|
||||
/// HTML format with visualizations
|
||||
Html,
|
||||
}
|
||||
|
||||
/// Result of a profiling operation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfilingResult {
|
||||
/// Name of the profiled operation
|
||||
pub name: String,
|
||||
/// Execution duration
|
||||
pub duration: Duration,
|
||||
/// GPU time (may differ from wall time)
|
||||
pub gpu_time: Duration,
|
||||
/// Memory bandwidth achieved (GB/s)
|
||||
pub bandwidth_gbps: f64,
|
||||
/// Compute throughput (GFLOPS)
|
||||
pub gflops: f64,
|
||||
/// Warp/SIMD occupancy (0.0 - 1.0)
|
||||
pub occupancy: f64,
|
||||
/// Memory used (bytes)
|
||||
pub memory_used: u64,
|
||||
/// Cache hit rate (0.0 - 1.0)
|
||||
pub cache_hit_rate: f64,
|
||||
/// Additional metrics
|
||||
pub extra_metrics: std::collections::HashMap<String, f64>,
|
||||
}
|
||||
|
||||
impl ProfilingResult {
|
||||
/// Get warp occupancy as percentage
|
||||
pub fn warp_occupancy(&self) -> f64 {
|
||||
self.occupancy
|
||||
}
|
||||
|
||||
/// Get achieved bandwidth in GB/s
|
||||
pub fn achieved_bandwidth_gbps(&self) -> f64 {
|
||||
self.bandwidth_gbps
|
||||
}
|
||||
|
||||
/// Check if the kernel achieved good occupancy (>75%)
|
||||
pub fn is_efficient(&self) -> bool {
|
||||
self.occupancy > 0.75 && self.cache_hit_rate > 0.8
|
||||
}
|
||||
|
||||
/// Format as string for display
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
"{}: {:.2}ms, {:.1}% occupancy, {:.1} GB/s, {:.1} GFLOPS",
|
||||
self.name,
|
||||
self.duration.as_secs_f64() * 1000.0,
|
||||
self.occupancy * 100.0,
|
||||
self.bandwidth_gbps,
|
||||
self.gflops
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProfilingResult {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
duration: Duration::ZERO,
|
||||
gpu_time: Duration::ZERO,
|
||||
bandwidth_gbps: 0.0,
|
||||
gflops: 0.0,
|
||||
occupancy: 0.0,
|
||||
memory_used: 0,
|
||||
cache_hit_rate: 0.0,
|
||||
extra_metrics: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended kernel profiler for RustyTorch
|
||||
///
|
||||
/// Provides detailed profiling of GPU kernels with warp analysis,
|
||||
/// memory bandwidth tracking, and performance recommendations.
|
||||
pub struct ExtendedKernelProfiler {
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
metal_profiler: Option<MetalProfiler>,
|
||||
|
||||
/// Collected profiling results
|
||||
results: Arc<RwLock<Vec<ProfilingResult>>>,
|
||||
|
||||
/// Whether profiling is enabled
|
||||
enabled: bool,
|
||||
|
||||
/// Minimum duration to record (filters out tiny operations)
|
||||
min_duration: Duration,
|
||||
}
|
||||
|
||||
impl ExtendedKernelProfiler {
|
||||
/// Create a new profiler
|
||||
pub fn new() -> Result<Self> {
|
||||
info!("Creating ExtendedKernelProfiler");
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
let metal_profiler = {
|
||||
match MetalProfiler::new() {
|
||||
Ok(p) => Some(p),
|
||||
Err(e) => {
|
||||
debug!("Metal profiler unavailable: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
metal_profiler,
|
||||
results: Arc::new(RwLock::new(Vec::new())),
|
||||
enabled: true,
|
||||
min_duration: Duration::from_micros(10),
|
||||
})
|
||||
}
|
||||
|
||||
/// Enable or disable profiling
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
/// Check if profiling is enabled
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Set minimum duration for recording
|
||||
pub fn set_min_duration(&mut self, duration: Duration) {
|
||||
self.min_duration = duration;
|
||||
}
|
||||
|
||||
/// Profile a kernel launch
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let profiler = ExtendedKernelProfiler::new()?;
|
||||
/// let result = profiler.profile_launch("matmul", || {
|
||||
/// tensor_a.matmul(&tensor_b)
|
||||
/// });
|
||||
/// println!("Occupancy: {:.1}%", result.warp_occupancy() * 100.0);
|
||||
/// ```
|
||||
#[instrument(skip(self, f), fields(name = %name))]
|
||||
pub fn profile_launch<F, R>(&self, name: &str, f: F) -> ProfilingResult
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
if !self.enabled {
|
||||
f();
|
||||
return ProfilingResult {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
let _span = span!(Level::TRACE, "kernel", name = name).entered();
|
||||
|
||||
// Start timing
|
||||
let start = Instant::now();
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
let session: Option<ProfilingSession> = self
|
||||
.metal_profiler
|
||||
.as_ref()
|
||||
.and_then(|p| p.begin_session(name).ok());
|
||||
|
||||
// Execute the operation
|
||||
let _result = f();
|
||||
|
||||
// Stop timing
|
||||
let duration = start.elapsed();
|
||||
|
||||
// End profiling session and get report
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
let report: Option<ProfilingReport> = session.and_then(|s| {
|
||||
self.metal_profiler
|
||||
.as_ref()
|
||||
.and_then(|p| p.end_session(s).ok())
|
||||
});
|
||||
|
||||
// Build result
|
||||
let mut profiling_result = ProfilingResult {
|
||||
name: name.to_string(),
|
||||
duration,
|
||||
gpu_time: duration, // Will be updated with actual GPU time if available
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
if let Some(metrics) = report {
|
||||
profiling_result.gpu_time = metrics.duration;
|
||||
profiling_result.occupancy = metrics.average_gpu_utilization();
|
||||
profiling_result.memory_used = metrics.peak_memory_usage() as u64;
|
||||
|
||||
// Calculate bandwidth from samples if available
|
||||
if let Some(sample) = metrics.samples.first() {
|
||||
let bandwidth_bytes =
|
||||
sample.counters.memory_read_bandwidth + sample.counters.memory_write_bandwidth;
|
||||
profiling_result.bandwidth_gbps = bandwidth_bytes as f64 / 1e9;
|
||||
profiling_result.cache_hit_rate = sample.counters.l1_cache_hit_rate;
|
||||
}
|
||||
}
|
||||
|
||||
// Record if above threshold
|
||||
if duration >= self.min_duration {
|
||||
let mut results = self.results.write();
|
||||
results.push(profiling_result.clone());
|
||||
}
|
||||
|
||||
profiling_result
|
||||
}
|
||||
|
||||
/// Profile an async operation
|
||||
#[instrument(skip(self, f), fields(name = %name))]
|
||||
pub async fn profile_async<F, Fut, R>(&self, name: &str, f: F) -> ProfilingResult
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = R>,
|
||||
{
|
||||
if !self.enabled {
|
||||
f().await;
|
||||
return ProfilingResult {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Execute the async operation
|
||||
let _result = f().await;
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
let profiling_result = ProfilingResult {
|
||||
name: name.to_string(),
|
||||
duration,
|
||||
gpu_time: duration,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if duration >= self.min_duration {
|
||||
let mut results = self.results.write();
|
||||
results.push(profiling_result.clone());
|
||||
}
|
||||
|
||||
profiling_result
|
||||
}
|
||||
|
||||
/// Get all collected results
|
||||
pub fn results(&self) -> Vec<ProfilingResult> {
|
||||
self.results.read().clone()
|
||||
}
|
||||
|
||||
/// Clear collected results
|
||||
pub fn clear(&self) {
|
||||
self.results.write().clear();
|
||||
}
|
||||
|
||||
/// Export profiling report
|
||||
pub fn export_report(&self, format: ReportFormat) -> String {
|
||||
let results = self.results.read();
|
||||
|
||||
match format {
|
||||
ReportFormat::Text => self.format_text(&results),
|
||||
ReportFormat::Json => self.format_json(&results),
|
||||
ReportFormat::Csv => self.format_csv(&results),
|
||||
ReportFormat::Html => self.format_html(&results),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_text(&self, results: &[ProfilingResult]) -> String {
|
||||
let mut output = String::new();
|
||||
output.push_str("=== Kernel Profiling Report ===\n\n");
|
||||
|
||||
for result in results {
|
||||
output.push_str(&format!(
|
||||
"Kernel: {}\n Duration: {:.3}ms\n GPU Time: {:.3}ms\n Occupancy: {:.1}%\n Bandwidth: {:.1} GB/s\n GFLOPS: {:.1}\n Memory: {} bytes\n\n",
|
||||
result.name,
|
||||
result.duration.as_secs_f64() * 1000.0,
|
||||
result.gpu_time.as_secs_f64() * 1000.0,
|
||||
result.occupancy * 100.0,
|
||||
result.bandwidth_gbps,
|
||||
result.gflops,
|
||||
result.memory_used,
|
||||
));
|
||||
}
|
||||
|
||||
// Summary statistics
|
||||
if !results.is_empty() {
|
||||
let total_time: f64 = results.iter().map(|r| r.duration.as_secs_f64()).sum();
|
||||
let avg_occupancy: f64 =
|
||||
results.iter().map(|r| r.occupancy).sum::<f64>() / results.len() as f64;
|
||||
|
||||
output.push_str(&format!(
|
||||
"=== Summary ===\nTotal Kernels: {}\nTotal Time: {:.3}ms\nAverage Occupancy: {:.1}%\n",
|
||||
results.len(),
|
||||
total_time * 1000.0,
|
||||
avg_occupancy * 100.0,
|
||||
));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn format_json(&self, results: &[ProfilingResult]) -> String {
|
||||
let entries: Vec<_> = results
|
||||
.iter()
|
||||
.map(|r| {
|
||||
serde_json::json!({
|
||||
"name": r.name,
|
||||
"duration_ms": r.duration.as_secs_f64() * 1000.0,
|
||||
"gpu_time_ms": r.gpu_time.as_secs_f64() * 1000.0,
|
||||
"occupancy": r.occupancy,
|
||||
"bandwidth_gbps": r.bandwidth_gbps,
|
||||
"gflops": r.gflops,
|
||||
"memory_used": r.memory_used,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string_pretty(&entries).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn format_csv(&self, results: &[ProfilingResult]) -> String {
|
||||
let mut output = String::from(
|
||||
"name,duration_ms,gpu_time_ms,occupancy,bandwidth_gbps,gflops,memory_used\n",
|
||||
);
|
||||
|
||||
for r in results {
|
||||
output.push_str(&format!(
|
||||
"{},{:.3},{:.3},{:.4},{:.2},{:.2},{}\n",
|
||||
r.name,
|
||||
r.duration.as_secs_f64() * 1000.0,
|
||||
r.gpu_time.as_secs_f64() * 1000.0,
|
||||
r.occupancy,
|
||||
r.bandwidth_gbps,
|
||||
r.gflops,
|
||||
r.memory_used,
|
||||
));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn format_html(&self, results: &[ProfilingResult]) -> String {
|
||||
let mut output = String::from(
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head><title>Kernel Profiling Report</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #4CAF50; color: white; }
|
||||
tr:nth-child(even) { background-color: #f2f2f2; }
|
||||
.good { color: green; }
|
||||
.bad { color: red; }
|
||||
</style>
|
||||
</head><body>
|
||||
<h1>Kernel Profiling Report</h1>
|
||||
<table>
|
||||
<tr><th>Kernel</th><th>Duration (ms)</th><th>Occupancy (%)</th><th>Bandwidth (GB/s)</th><th>GFLOPS</th></tr>
|
||||
"#,
|
||||
);
|
||||
|
||||
for r in results {
|
||||
let occupancy_class = if r.occupancy > 0.75 { "good" } else { "bad" };
|
||||
output.push_str(&format!(
|
||||
"<tr><td>{}</td><td>{:.3}</td><td class=\"{}\">{:.1}</td><td>{:.1}</td><td>{:.1}</td></tr>\n",
|
||||
r.name,
|
||||
r.duration.as_secs_f64() * 1000.0,
|
||||
occupancy_class,
|
||||
r.occupancy * 100.0,
|
||||
r.bandwidth_gbps,
|
||||
r.gflops,
|
||||
));
|
||||
}
|
||||
|
||||
output.push_str("</table></body></html>");
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ExtendedKernelProfiler {
|
||||
fn default() -> Self {
|
||||
Self::new().unwrap_or_else(|_| Self {
|
||||
#[cfg(all(target_os = "macos", feature = "profiling-metal"))]
|
||||
metal_profiler: None,
|
||||
results: Arc::new(RwLock::new(Vec::new())),
|
||||
enabled: false,
|
||||
min_duration: Duration::from_micros(10),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ExtendedKernelProfiler {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ExtendedKernelProfiler")
|
||||
.field("enabled", &self.enabled)
|
||||
.field("results_count", &self.results.read().len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_profiler_creation() {
|
||||
let profiler = ExtendedKernelProfiler::new();
|
||||
assert!(profiler.is_ok() || cfg!(not(feature = "profiling-metal")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profiling_result_default() {
|
||||
let result = ProfilingResult::default();
|
||||
assert_eq!(result.name, "");
|
||||
assert_eq!(result.occupancy, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profiling_result_summary() {
|
||||
let result = ProfilingResult {
|
||||
name: "test_kernel".to_string(),
|
||||
duration: Duration::from_millis(10),
|
||||
occupancy: 0.85,
|
||||
bandwidth_gbps: 100.0,
|
||||
gflops: 1000.0,
|
||||
..Default::default()
|
||||
};
|
||||
let summary = result.summary();
|
||||
assert!(summary.contains("test_kernel"));
|
||||
assert!(summary.contains("85.0%"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_formats() {
|
||||
let profiler = ExtendedKernelProfiler::default();
|
||||
|
||||
// Test each format
|
||||
let text = profiler.export_report(ReportFormat::Text);
|
||||
assert!(text.contains("Kernel Profiling Report"));
|
||||
|
||||
let csv = profiler.export_report(ReportFormat::Csv);
|
||||
assert!(csv.contains("name,duration_ms"));
|
||||
|
||||
let html = profiler.export_report(ReportFormat::Html);
|
||||
assert!(html.contains("<!DOCTYPE html>"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user