Packages
snakepit
0.10.1
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
High-performance pooler and session manager for external language integrations. Supports Python, Node.js, Ruby, and more with gRPC streaming, session management, and production-ready process cleanup.
Current section
Files
Jump to
Current section
Files
lib/snakepit/telemetry/gpu_profiler.ex
defmodule Snakepit.Telemetry.GPUProfiler do
@moduledoc """
GPU memory and utilization profiler.
Periodically samples GPU metrics and emits telemetry events.
Supports NVIDIA CUDA GPUs via nvidia-smi.
"""
use GenServer
require Logger
alias Snakepit.Hardware
@default_interval_ms 5_000
@min_interval_ms 100
@type state :: %{
interval_ms: pos_integer(),
enabled: boolean(),
sample_count: non_neg_integer(),
last_sample_time: integer() | nil,
timer_ref: reference() | nil,
devices: [Hardware.Selector.device()]
}
# Client API
@doc """
Starts the GPU profiler.
## Options
- `:interval_ms` - Sampling interval in milliseconds (default: 5000)
- `:enabled` - Whether to start sampling immediately (default: true)
- `:name` - GenServer name (default: __MODULE__)
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@doc """
Triggers an immediate GPU sample.
"""
@spec sample_now(GenServer.server()) :: :ok | {:error, :no_gpu}
def sample_now(server \\ __MODULE__) do
GenServer.call(server, :sample_now)
end
@doc """
Returns profiler statistics.
"""
@spec get_stats(GenServer.server()) :: map()
def get_stats(server \\ __MODULE__) do
GenServer.call(server, :get_stats)
end
@doc """
Enables GPU sampling.
"""
@spec enable(GenServer.server()) :: :ok
def enable(server \\ __MODULE__) do
GenServer.call(server, :enable)
end
@doc """
Disables GPU sampling.
"""
@spec disable(GenServer.server()) :: :ok
def disable(server \\ __MODULE__) do
GenServer.call(server, :disable)
end
@doc """
Updates the sampling interval.
"""
@spec set_interval(GenServer.server(), pos_integer()) :: :ok | {:error, :invalid_interval}
def set_interval(server \\ __MODULE__, interval_ms) do
GenServer.call(server, {:set_interval, interval_ms})
end
# GenServer callbacks
@impl true
def init(opts) do
interval_ms = Keyword.get(opts, :interval_ms, @default_interval_ms)
enabled = Keyword.get(opts, :enabled, true)
state = %{
interval_ms: interval_ms,
enabled: enabled,
sample_count: 0,
last_sample_time: nil,
timer_ref: nil,
devices: detect_gpu_devices()
}
state =
if enabled and state.devices != [] do
schedule_sample(state)
else
state
end
{:ok, state}
end
@impl true
def handle_call(:sample_now, _from, state) do
case do_sample(state) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_call(:get_stats, _from, state) do
stats = %{
interval_ms: state.interval_ms,
enabled: state.enabled,
sample_count: state.sample_count,
last_sample_time: state.last_sample_time,
device_count: length(state.devices)
}
{:reply, stats, state}
end
def handle_call(:enable, _from, state) do
state = %{state | enabled: true}
state = if state.devices != [], do: schedule_sample(state), else: state
{:reply, :ok, state}
end
def handle_call(:disable, _from, state) do
state = cancel_timer(state)
state = %{state | enabled: false}
{:reply, :ok, state}
end
def handle_call({:set_interval, interval_ms}, _from, state)
when interval_ms >= @min_interval_ms do
state = cancel_timer(state)
state = %{state | interval_ms: interval_ms}
state = if state.enabled and state.devices != [], do: schedule_sample(state), else: state
{:reply, :ok, state}
end
def handle_call({:set_interval, _}, _from, state) do
{:reply, {:error, :invalid_interval}, state}
end
@impl true
def handle_info(:sample, state) do
state =
case do_sample(state) do
{:ok, new_state} -> new_state
{:error, _} -> state
end
state = if state.enabled, do: schedule_sample(state), else: state
{:noreply, state}
end
def handle_info(_msg, state) do
{:noreply, state}
end
# Private functions
defp detect_gpu_devices do
info = Hardware.detect()
case info.cuda do
%{devices: devices} when is_list(devices) and devices != [] ->
Enum.map(devices, fn d -> {:cuda, d.id} end)
_ ->
[]
end
end
defp schedule_sample(state) do
state = cancel_timer(state)
ref = Process.send_after(self(), :sample, state.interval_ms)
%{state | timer_ref: ref}
end
defp cancel_timer(%{timer_ref: nil} = state), do: state
defp cancel_timer(%{timer_ref: ref} = state) do
Process.cancel_timer(ref)
%{state | timer_ref: nil}
end
defp do_sample(%{devices: []} = _state) do
{:error, :no_gpu}
end
defp do_sample(state) do
now = System.monotonic_time(:millisecond)
Enum.each(state.devices, fn device ->
case sample_device(device) do
{:ok, metrics} ->
emit_metrics(device, metrics)
{:error, _reason} ->
:ok
end
end)
new_state = %{
state
| sample_count: state.sample_count + 1,
last_sample_time: now
}
{:ok, new_state}
end
defp sample_device({:cuda, device_id}) do
query = [
"--id=#{device_id}",
"--query-gpu=memory.used,memory.total,memory.free,utilization.gpu,temperature.gpu,power.draw",
"--format=csv,noheader,nounits"
]
case System.cmd("nvidia-smi", query, stderr_to_stdout: true) do
{output, 0} ->
parse_nvidia_smi_output(output)
_ ->
{:error, :nvidia_smi_failed}
end
rescue
_ -> {:error, :nvidia_smi_not_found}
end
defp sample_device(_), do: {:error, :unsupported_device}
defp parse_nvidia_smi_output(output) do
case String.split(String.trim(output), ", ") do
[used, total, free, gpu_util, temp, power] ->
{:ok,
%{
memory_used_mb: parse_int(used),
memory_total_mb: parse_int(total),
memory_free_mb: parse_int(free),
gpu_utilization: parse_float(gpu_util),
temperature: parse_float(temp),
power_watts: parse_float(power)
}}
_ ->
{:error, :parse_error}
end
end
defp parse_int(str) do
case Integer.parse(String.trim(str)) do
{n, _} -> n
:error -> 0
end
end
defp parse_float(str) do
case Float.parse(String.trim(str)) do
{f, _} -> f
:error -> 0.0
end
end
defp emit_metrics(device, metrics) do
# Memory event
:telemetry.execute(
[:snakepit, :gpu, :memory, :sampled],
%{
used_mb: metrics.memory_used_mb,
total_mb: metrics.memory_total_mb,
free_mb: metrics.memory_free_mb
},
%{
device: device,
utilization: metrics.memory_used_mb / max(metrics.memory_total_mb, 1)
}
)
# Utilization event
:telemetry.execute(
[:snakepit, :gpu, :utilization, :sampled],
%{
gpu_percent: metrics.gpu_utilization,
memory_percent: metrics.memory_used_mb / max(metrics.memory_total_mb, 1) * 100
},
%{device: device}
)
# Temperature event
if metrics.temperature > 0 do
:telemetry.execute(
[:snakepit, :gpu, :temperature, :sampled],
%{celsius: metrics.temperature},
%{device: device}
)
end
# Power event
if metrics.power_watts > 0 do
:telemetry.execute(
[:snakepit, :gpu, :power, :sampled],
%{watts: metrics.power_watts, limit_watts: 0.0},
%{device: device}
)
end
end
end