Packages

MLX machine learning framework bindings for Erlang

Current section

Files

Jump to
mlx src mlx_profiler.erl
Raw

src/mlx_profiler.erl

-module(mlx_profiler).
%% Comprehensive performance monitoring and optimization for MLX
-export([
%% Profiling control
start_profiling/0, start_profiling/1,
stop_profiling/0, get_profile_stats/0,
reset_stats/0,
%% Memory profiling
memory_snapshot/0, memory_diff/2,
track_memory_usage/1, memory_timeline/0,
detect_memory_leaks/0,
%% Compute profiling
profile_computation/1, profile_computation/2,
benchmark_operation/2, benchmark_operation/3,
compare_implementations/2,
%% GPU profiling
gpu_utilization/0, gpu_memory_usage/0,
gpu_kernel_stats/0, gpu_timeline/0,
%% Performance optimization
optimize_computation_graph/1, optimize_computation_graph/2,
suggest_optimizations/1,
auto_tune_parameters/2,
%% Bottleneck analysis
find_bottlenecks/1, analyze_performance/1,
generate_performance_report/1,
%% Real-time monitoring
start_monitoring/0, start_monitoring/1,
stop_monitoring/0, get_monitoring_data/0,
%% Debugging utilities
trace_execution/1, trace_execution/2,
debug_tensor_flow/1,
validate_numerics/1,
%% Performance metrics
throughput_analysis/2, latency_analysis/2,
efficiency_metrics/1, scalability_analysis/2
]).
-record(profile_stats, {
start_time,
operations = #{},
memory_usage = [],
gpu_stats = #{},
timeline = []
}).
-record(operation_stats, {
count = 0,
total_time = 0.0,
min_time = infinity,
max_time = 0.0,
memory_allocated = 0,
memory_peak = 0
}).
%% Profiling control
start_profiling() ->
start_profiling(#{}).
start_profiling(Options) ->
EnableMemory = maps:get(memory, Options, true),
EnableGPU = maps:get(gpu, Options, true),
EnableTimeline = maps:get(timeline, Options, true),
SamplingInterval = maps:get(sampling_interval, Options, 100), % ms
Stats = #profile_stats{
start_time = erlang:system_time(microsecond)
},
put(profiling_enabled, true),
put(profile_stats, Stats),
put(profiling_options, Options),
% Start background monitoring
case EnableMemory of
true -> start_memory_monitor(SamplingInterval);
false -> ok
end,
case EnableGPU of
true -> start_gpu_monitor(SamplingInterval);
false -> ok
end,
ok.
stop_profiling() ->
put(profiling_enabled, false),
stop_memory_monitor(),
stop_gpu_monitor(),
ok.
get_profile_stats() ->
case get(profile_stats) of
undefined -> {error, profiling_not_started};
Stats -> {ok, Stats}
end.
reset_stats() ->
case get(profiling_enabled) of
true ->
Stats = #profile_stats{
start_time = erlang:system_time(microsecond)
},
put(profile_stats, Stats),
ok;
_ ->
{error, profiling_not_enabled}
end.
%% Memory profiling
memory_snapshot() ->
% Take snapshot of current memory usage
#{
timestamp => erlang:system_time(microsecond),
mlx_memory => mlx_nif:memory_info(),
system_memory => erlang:memory(),
gpu_memory => gpu_memory_usage()
}.
memory_diff(Snapshot1, Snapshot2) ->
% Calculate memory difference between snapshots
MLXDiff = maps:get(mlx_memory, Snapshot2) - maps:get(mlx_memory, Snapshot1),
SystemDiff = calculate_memory_diff(maps:get(system_memory, Snapshot2),
maps:get(system_memory, Snapshot1)),
GPUDiff = maps:get(gpu_memory, Snapshot2) - maps:get(gpu_memory, Snapshot1),
#{
mlx_memory_diff => MLXDiff,
system_memory_diff => SystemDiff,
gpu_memory_diff => GPUDiff,
time_diff => maps:get(timestamp, Snapshot2) - maps:get(timestamp, Snapshot1)
}.
track_memory_usage(Fun) ->
% Track memory usage during function execution
Before = memory_snapshot(),
Result = Fun(),
After = memory_snapshot(),
Diff = memory_diff(Before, After),
{Result, Diff}.
memory_timeline() ->
% Get memory usage timeline
case get(memory_timeline) of
undefined -> [];
Timeline -> Timeline
end.
detect_memory_leaks() ->
% Analyze memory timeline for potential leaks
Timeline = memory_timeline(),
case length(Timeline) < 10 of
true -> {error, insufficient_data};
false ->
% Analyze growth trend
MemoryValues = [maps:get(mlx_memory, Snapshot) || Snapshot <- Timeline],
Trend = calculate_trend(MemoryValues),
case Trend > 0.1 of % 10% growth threshold
true -> {warning, potential_memory_leak, Trend};
false -> {ok, no_leak_detected}
end
end.
%% Compute profiling
profile_computation(Fun) ->
profile_computation(Fun, #{}).
profile_computation(Fun, Options) ->
% Profile computation with detailed metrics
EnableTrace = maps:get(trace, Options, false),
EnableMemory = maps:get(memory, Options, true),
% Setup profiling
case EnableTrace of
true -> mlx_nif:enable_tracing();
false -> ok
end,
% Memory snapshot before
MemoryBefore = case EnableMemory of
true -> memory_snapshot();
false -> undefined
end,
% Time execution
StartTime = erlang:system_time(microsecond),
Result = Fun(),
EndTime = erlang:system_time(microsecond),
% Memory snapshot after
MemoryAfter = case EnableMemory of
true -> memory_snapshot();
false -> undefined
end,
% Collect profiling data
ProfileData = #{
execution_time => EndTime - StartTime,
result => Result,
memory_usage => case {MemoryBefore, MemoryAfter} of
{undefined, undefined} -> undefined;
_ -> memory_diff(MemoryBefore, MemoryAfter)
end,
trace_data => case EnableTrace of
true -> mlx_nif:get_trace_data();
false -> undefined
end
},
% Cleanup
case EnableTrace of
true -> mlx_nif:disable_tracing();
false -> ok
end,
{ok, ProfileData}.
benchmark_operation(Operation, NumRuns) ->
benchmark_operation(Operation, NumRuns, #{}).
benchmark_operation(Operation, NumRuns, Options) ->
% Benchmark operation multiple times
WarmupRuns = maps:get(warmup, Options, erlang:max(1, NumRuns div 10)),
% Warmup
lists:foreach(fun(_) -> Operation() end, lists:seq(1, WarmupRuns)),
% Actual benchmark
Times = lists:map(fun(_) ->
StartTime = erlang:system_time(microsecond),
_Result = Operation(),
EndTime = erlang:system_time(microsecond),
EndTime - StartTime
end, lists:seq(1, NumRuns)),
% Calculate statistics
MinTime = lists:min(Times),
MaxTime = lists:max(Times),
MeanTime = lists:sum(Times) / length(Times),
MedianTime = lists:nth((length(Times) + 1) div 2, lists:sort(Times)),
StdDev = math:sqrt(lists:sum([(T - MeanTime) * (T - MeanTime) || T <- Times]) / length(Times)),
#{
num_runs => NumRuns,
min_time => MinTime,
max_time => MaxTime,
mean_time => MeanTime,
median_time => MedianTime,
std_dev => StdDev,
throughput => 1000000 / MeanTime % ops per second
}.
compare_implementations(Implementations, NumRuns) ->
% Compare multiple implementations
Results = maps:map(fun(Name, Implementation) ->
io:format("Benchmarking ~s...~n", [Name]),
benchmark_operation(Implementation, NumRuns)
end, Implementations),
% Find fastest implementation
BestName = maps:fold(fun(Name, Stats, AccName) ->
case AccName of
undefined -> Name;
_ ->
AccStats = maps:get(AccName, Results),
case maps:get(mean_time, Stats) < maps:get(mean_time, AccStats) of
true -> Name;
false -> AccName
end
end
end, undefined, Results),
#{
results => Results,
fastest => BestName,
speedup_ratios => calculate_speedup_ratios(Results, BestName)
}.
%% GPU profiling
gpu_utilization() ->
% Get current GPU utilization
mlx_nif:gpu_utilization().
gpu_memory_usage() ->
% Get current GPU memory usage
mlx_nif:gpu_memory_usage().
gpu_kernel_stats() ->
% Get GPU kernel execution statistics
mlx_nif:gpu_kernel_stats().
gpu_timeline() ->
% Get GPU execution timeline
case get(gpu_timeline) of
undefined -> [];
Timeline -> Timeline
end.
%% Performance optimization
optimize_computation_graph(ComputationGraph) ->
optimize_computation_graph(ComputationGraph, #{}).
optimize_computation_graph(ComputationGraph, Options) ->
% Optimize computation graph for performance
Optimizations = maps:get(optimizations, Options, [
constant_folding,
dead_code_elimination,
operator_fusion,
memory_layout_optimization
]),
OptimizedGraph = lists:foldl(fun(Optimization, Graph) ->
apply_optimization(Optimization, Graph)
end, ComputationGraph, Optimizations),
{ok, OptimizedGraph}.
suggest_optimizations(ProfileData) ->
% Analyze profile data and suggest optimizations
Suggestions = [],
% Check for memory inefficiencies
MemorySuggestions = case maps:get(memory_usage, ProfileData, undefined) of
undefined -> [];
MemoryData ->
analyze_memory_patterns(MemoryData)
end,
% Check for compute inefficiencies
ComputeSuggestions = case maps:get(trace_data, ProfileData, undefined) of
undefined -> [];
TraceData ->
analyze_compute_patterns(TraceData)
end,
MemorySuggestions ++ ComputeSuggestions ++ Suggestions.
auto_tune_parameters(Model, ValidationData) ->
% Automatically tune model parameters for optimal performance
Parameters = [
{batch_size, [16, 32, 64, 128, 256]},
{learning_rate, [0.1, 0.01, 0.001, 0.0001]},
{optimizer, [adam, sgd, rmsprop]}
],
BestConfig = grid_search_optimization(Model, ValidationData, Parameters),
{ok, BestConfig}.
%% Bottleneck analysis
find_bottlenecks(ProfileData) ->
% Identify performance bottlenecks
TraceData = maps:get(trace_data, ProfileData, []),
MemoryData = maps:get(memory_usage, ProfileData, #{}),
% Analyze execution time bottlenecks
TimeBottlenecks = analyze_time_bottlenecks(TraceData),
% Analyze memory bottlenecks
MemoryBottlenecks = analyze_memory_bottlenecks(MemoryData),
#{
time_bottlenecks => TimeBottlenecks,
memory_bottlenecks => MemoryBottlenecks,
recommendations => generate_recommendations(TimeBottlenecks, MemoryBottlenecks)
}.
analyze_performance(Model) ->
% Comprehensive performance analysis
{ok, ProfileData} = profile_computation(fun() ->
% Run model inference
{ok, TestInput} = mlx:random([1, 224, 224, 3]),
Model(TestInput)
end, #{trace => true, memory => true}),
Bottlenecks = find_bottlenecks(ProfileData),
Suggestions = suggest_optimizations(ProfileData),
#{
profile_data => ProfileData,
bottlenecks => Bottlenecks,
optimization_suggestions => Suggestions
}.
generate_performance_report(AnalysisResults) ->
% Generate comprehensive performance report
Report = [
"=== MLX Performance Analysis Report ===\n",
format_execution_summary(AnalysisResults),
format_bottleneck_analysis(AnalysisResults),
format_optimization_suggestions(AnalysisResults),
format_recommendations(AnalysisResults)
],
ReportString = lists:flatten(Report),
{ok, ReportString}.
%% Real-time monitoring
start_monitoring() ->
start_monitoring(#{}).
start_monitoring(Options) ->
% Start real-time performance monitoring
Interval = maps:get(interval, Options, 1000), % ms
Metrics = maps:get(metrics, Options, [memory, gpu, throughput]),
MonitorPid = spawn_link(fun() -> monitoring_loop(Interval, Metrics) end),
put(monitor_pid, MonitorPid),
ok.
stop_monitoring() ->
case get(monitor_pid) of
undefined -> ok;
Pid ->
Pid ! stop,
erase(monitor_pid),
ok
end.
get_monitoring_data() ->
case get(monitoring_data) of
undefined -> [];
Data -> Data
end.
%% Debugging utilities
trace_execution(Fun) ->
trace_execution(Fun, #{}).
trace_execution(Fun, Options) ->
% Trace execution with detailed information
LogLevel = maps:get(log_level, Options, debug),
IncludeStackTrace = maps:get(stack_trace, Options, false),
mlx_nif:set_trace_level(LogLevel),
mlx_nif:enable_tracing(),
try
Result = Fun(),
TraceData = mlx_nif:get_trace_data(),
case IncludeStackTrace of
true ->
StackTrace = erlang:get_stacktrace(),
{Result, TraceData, StackTrace};
false ->
{Result, TraceData}
end
after
mlx_nif:disable_tracing()
end.
debug_tensor_flow(Tensors) ->
% Debug tensor data flow
lists:map(fun(Tensor) ->
{ok, Shape} = mlx:shape(Tensor),
{ok, Dtype} = mlx:dtype(Tensor),
% Check for common issues
Issues = check_tensor_issues(Tensor),
#{
shape => Shape,
dtype => Dtype,
issues => Issues,
stats => compute_tensor_stats(Tensor)
}
end, Tensors).
validate_numerics(Tensor) ->
% Validate tensor for numerical issues
{ok, HasNaN} = mlx:any(mlx:isnan(Tensor)),
{ok, HasInf} = mlx:any(mlx:isinf(Tensor)),
{ok, Min} = mlx:min(Tensor),
{ok, Max} = mlx:max(Tensor),
Issues = [],
Issues1 = case HasNaN of
true -> [nan_detected | Issues];
false -> Issues
end,
Issues2 = case HasInf of
true -> [inf_detected | Issues1];
false -> Issues1
end,
#{
has_nan => HasNaN,
has_inf => HasInf,
min_value => Min,
max_value => Max,
issues => Issues2
}.
%% Performance metrics
throughput_analysis(Model, DataLoader) ->
% Analyze model throughput
NumBatches = 100,
StartTime = erlang:system_time(microsecond),
TotalSamples = lists:foldl(fun(Batch, Acc) ->
_Output = Model(Batch),
{ok, BatchSize} = mlx:shape(Batch),
Acc + element(1, BatchSize)
end, 0, lists:sublist(DataLoader, NumBatches)),
EndTime = erlang:system_time(microsecond),
TotalTime = (EndTime - StartTime) / 1000000, % seconds
#{
samples_per_second => TotalSamples / TotalTime,
total_samples => TotalSamples,
total_time => TotalTime,
batches_processed => NumBatches
}.
latency_analysis(Model, TestInputs) ->
% Analyze model latency
Latencies = lists:map(fun(Input) ->
StartTime = erlang:system_time(microsecond),
_Output = Model(Input),
EndTime = erlang:system_time(microsecond),
EndTime - StartTime
end, TestInputs),
MinLatency = lists:min(Latencies),
MaxLatency = lists:max(Latencies),
MeanLatency = lists:sum(Latencies) / length(Latencies),
P50 = percentile(Latencies, 50),
P95 = percentile(Latencies, 95),
P99 = percentile(Latencies, 99),
#{
min_latency => MinLatency,
max_latency => MaxLatency,
mean_latency => MeanLatency,
p50_latency => P50,
p95_latency => P95,
p99_latency => P99
}.
efficiency_metrics(Model) ->
% Compute efficiency metrics
#{
flops => estimate_flops(Model),
params => count_parameters(Model),
memory_footprint => estimate_memory_footprint(Model),
efficiency_score => compute_efficiency_score(Model)
}.
scalability_analysis(Model, BatchSizes) ->
% Analyze model scalability across batch sizes
Results = lists:map(fun(BatchSize) ->
{ok, TestInput} = mlx:random([BatchSize, 224, 224, 3]),
{ThroughputResult, _} = track_memory_usage(fun() ->
benchmark_operation(fun() -> Model(TestInput) end, 10)
end),
#{
batch_size => BatchSize,
throughput => maps:get(throughput, ThroughputResult),
mean_time => maps:get(mean_time, ThroughputResult)
}
end, BatchSizes),
#{
results => Results,
scaling_efficiency => compute_scaling_efficiency(Results)
}.
%% Helper functions
start_memory_monitor(Interval) ->
MonitorPid = spawn_link(fun() -> memory_monitor_loop(Interval) end),
put(memory_monitor_pid, MonitorPid).
stop_memory_monitor() ->
case get(memory_monitor_pid) of
undefined -> ok;
Pid -> Pid ! stop, erase(memory_monitor_pid)
end.
start_gpu_monitor(Interval) ->
MonitorPid = spawn_link(fun() -> gpu_monitor_loop(Interval) end),
put(gpu_monitor_pid, MonitorPid).
stop_gpu_monitor() ->
case get(gpu_monitor_pid) of
undefined -> ok;
Pid -> Pid ! stop, erase(gpu_monitor_pid)
end.
memory_monitor_loop(Interval) ->
receive
stop -> ok
after Interval ->
Snapshot = memory_snapshot(),
Timeline = case get(memory_timeline) of
undefined -> [Snapshot];
Existing -> [Snapshot | lists:sublist(Existing, 999)] % Keep last 1000
end,
put(memory_timeline, Timeline),
memory_monitor_loop(Interval)
end.
gpu_monitor_loop(Interval) ->
receive
stop -> ok
after Interval ->
GPUStats = #{
timestamp => erlang:system_time(microsecond),
utilization => gpu_utilization(),
memory => gpu_memory_usage()
},
Timeline = case get(gpu_timeline) of
undefined -> [GPUStats];
Existing -> [GPUStats | lists:sublist(Existing, 999)]
end,
put(gpu_timeline, Timeline),
gpu_monitor_loop(Interval)
end.
monitoring_loop(Interval, Metrics) ->
receive
stop -> ok
after Interval ->
Data = collect_monitoring_metrics(Metrics),
MonitoringData = case get(monitoring_data) of
undefined -> [Data];
Existing -> [Data | lists:sublist(Existing, 999)]
end,
put(monitoring_data, MonitoringData),
monitoring_loop(Interval, Metrics)
end.
collect_monitoring_metrics(Metrics) ->
lists:foldl(fun(Metric, Acc) ->
Value = case Metric of
memory -> memory_snapshot();
gpu -> #{utilization => gpu_utilization(), memory => gpu_memory_usage()};
throughput -> get_current_throughput()
end,
maps:put(Metric, Value, Acc)
end, #{timestamp => erlang:system_time(microsecond)}, Metrics).
calculate_memory_diff(Memory1, Memory2) ->
Keys = [total, processes, system, atom, binary, ets],
maps:from_list([{Key, proplists:get_value(Key, Memory2, 0) -
proplists:get_value(Key, Memory1, 0)} || Key <- Keys]).
calculate_trend(Values) ->
N = length(Values),
case N < 2 of
true -> 0.0;
false ->
X = lists:seq(1, N),
SumX = lists:sum(X),
SumY = lists:sum(Values),
SumXY = lists:sum([Xi * Yi || {Xi, Yi} <- lists:zip(X, Values)]),
SumX2 = lists:sum([Xi * Xi || Xi <- X]),
Slope = (N * SumXY - SumX * SumY) / (N * SumX2 - SumX * SumX),
Slope / lists:nth(1, Values) % Relative trend
end.
apply_optimization(constant_folding, Graph) ->
% Apply constant folding optimization
Graph; % Simplified
apply_optimization(dead_code_elimination, Graph) ->
% Remove dead code
Graph; % Simplified
apply_optimization(operator_fusion, Graph) ->
% Fuse operators
Graph; % Simplified
apply_optimization(memory_layout_optimization, Graph) ->
% Optimize memory layout
Graph. % Simplified
analyze_memory_patterns(MemoryData) ->
% Analyze memory usage patterns
[]. % Simplified
analyze_compute_patterns(TraceData) ->
% Analyze computation patterns
[]. % Simplified
grid_search_optimization(Model, ValidationData, Parameters) ->
% Grid search parameter optimization
#{}. % Simplified
analyze_time_bottlenecks(TraceData) ->
% Analyze execution time bottlenecks
[]. % Simplified
analyze_memory_bottlenecks(MemoryData) ->
% Analyze memory bottlenecks
[]. % Simplified
generate_recommendations(TimeBottlenecks, MemoryBottlenecks) ->
% Generate optimization recommendations
[]. % Simplified
format_execution_summary(AnalysisResults) ->
"Execution Summary:\n". % Simplified
format_bottleneck_analysis(AnalysisResults) ->
"Bottleneck Analysis:\n". % Simplified
format_optimization_suggestions(AnalysisResults) ->
"Optimization Suggestions:\n". % Simplified
format_recommendations(AnalysisResults) ->
"Recommendations:\n". % Simplified
check_tensor_issues(Tensor) ->
[]. % Simplified
compute_tensor_stats(Tensor) ->
#{}. % Simplified
percentile(Values, P) ->
SortedValues = lists:sort(Values),
Index = round(P / 100 * length(SortedValues)),
lists:nth(erlang:max(1, Index), SortedValues).
estimate_flops(Model) ->
0. % Simplified
count_parameters(Model) ->
0. % Simplified
estimate_memory_footprint(Model) ->
0. % Simplified
compute_efficiency_score(Model) ->
0.0. % Simplified
compute_scaling_efficiency(Results) ->
1.0. % Simplified
get_current_throughput() ->
0.0. % Simplified
calculate_speedup_ratios(Results, BestName) ->
BestTime = maps:get(mean_time, maps:get(BestName, Results)),
maps:map(fun(_Name, Stats) ->
maps:get(mean_time, Stats) / BestTime
end, Results).