MFU metrics in Prometheus (#19395)
This commit is contained in:
@@ -185,6 +185,7 @@ Please consult the documentation below and [server_args.py](https://github.com/s
|
|||||||
| `--crash-dump-folder` | Folder path to dump requests from the last 5 min before a crash (if any). If not specified, crash dumping is disabled. | `None` | Type: str |
|
| `--crash-dump-folder` | Folder path to dump requests from the last 5 min before a crash (if any). If not specified, crash dumping is disabled. | `None` | Type: str |
|
||||||
| `--show-time-cost` | Show time cost of custom marks. | `False` | bool flag (set to enable) |
|
| `--show-time-cost` | Show time cost of custom marks. | `False` | bool flag (set to enable) |
|
||||||
| `--enable-metrics` | Enable log prometheus metrics. | `False` | bool flag (set to enable) |
|
| `--enable-metrics` | Enable log prometheus metrics. | `False` | bool flag (set to enable) |
|
||||||
|
| `--enable-mfu-metrics` | Enable estimated MFU-related prometheus metrics. | `False` | bool flag (set to enable) |
|
||||||
| `--enable-metrics-for-all-schedulers` | Enable --enable-metrics-for-all-schedulers when you want schedulers on all TP ranks (not just TP 0) to record request metrics separately. This is especially useful when dp_attention is enabled, as otherwise all metrics appear to come from TP 0. | `False` | bool flag (set to enable) |
|
| `--enable-metrics-for-all-schedulers` | Enable --enable-metrics-for-all-schedulers when you want schedulers on all TP ranks (not just TP 0) to record request metrics separately. This is especially useful when dp_attention is enabled, as otherwise all metrics appear to come from TP 0. | `False` | bool flag (set to enable) |
|
||||||
| `--tokenizer-metrics-custom-labels-header` | Specify the HTTP header for passing custom labels for tokenizer metrics. | `x-custom-labels` | Type: str |
|
| `--tokenizer-metrics-custom-labels-header` | Specify the HTTP header for passing custom labels for tokenizer metrics. | `x-custom-labels` | Type: str |
|
||||||
| `--tokenizer-metrics-allowed-custom-labels` | The custom labels allowed for tokenizer metrics. The labels are specified via a dict in '--tokenizer-metrics-custom-labels-header' field in HTTP requests, e.g., {'label1': 'value1', 'label2': 'value2'} is allowed if '--tokenizer-metrics-allowed-custom-labels label1 label2' is set. | `None` | List[str] |
|
| `--tokenizer-metrics-allowed-custom-labels` | The custom labels allowed for tokenizer metrics. The labels are specified via a dict in '--tokenizer-metrics-custom-labels-header' field in HTTP requests, e.g., {'label1': 'value1', 'label2': 'value2'} is allowed if '--tokenizer-metrics-allowed-custom-labels label1 label2' is set. | `None` | List[str] |
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ This section describes how to set up the monitoring stack (Prometheus + Grafana)
|
|||||||
python -m sglang.launch_server \
|
python -m sglang.launch_server \
|
||||||
--model-path <your_model_path> \
|
--model-path <your_model_path> \
|
||||||
--port 30000 \
|
--port 30000 \
|
||||||
--enable-metrics
|
--enable-metrics \
|
||||||
|
--enable-mfu-metrics
|
||||||
```
|
```
|
||||||
Replace `<your_model_path>` with the actual path to your model (e.g., `meta-llama/Meta-Llama-3.1-8B-Instruct`). Ensure the server is accessible from the monitoring stack (you might need `--host 0.0.0.0` if running in Docker). By default, the metrics endpoint will be available at `http://<sglang_server_host>:30000/metrics`.
|
Replace `<your_model_path>` with the actual path to your model (e.g., `meta-llama/Meta-Llama-3.1-8B-Instruct`). Ensure the server is accessible from the monitoring stack (you might need `--host 0.0.0.0` if running in Docker). By default, the metrics endpoint will be available at `http://<sglang_server_host>:30000/metrics`.
|
||||||
|
|
||||||
@@ -229,3 +230,38 @@ python3 -m sglang.bench_serving \
|
|||||||
to generate some requests.
|
to generate some requests.
|
||||||
|
|
||||||
Then you should be able to see the metrics in the Grafana dashboard.
|
Then you should be able to see the metrics in the Grafana dashboard.
|
||||||
|
|
||||||
|
## Estimated Performance Metrics (MFU-related)
|
||||||
|
|
||||||
|
SGLang exports the following estimated per-GPU counters that can be used to derive
|
||||||
|
Model FLOPs Utilization (MFU)-related signals:
|
||||||
|
|
||||||
|
- `sglang:estimated_flops_per_gpu_total`: Estimated floating-point operations.
|
||||||
|
- `sglang:estimated_read_bytes_per_gpu_total`: Estimated bytes read from memory.
|
||||||
|
- `sglang:estimated_write_bytes_per_gpu_total`: Estimated bytes written to memory.
|
||||||
|
|
||||||
|
These metrics are available when both `--enable-metrics` and
|
||||||
|
`--enable-mfu-metrics` are enabled.
|
||||||
|
|
||||||
|
These are cumulative counters. Use Prometheus `rate(...)` to get per-second values.
|
||||||
|
|
||||||
|
### PromQL examples
|
||||||
|
|
||||||
|
Average TFLOPS per GPU:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
rate(sglang:estimated_flops_per_gpu_total[1m]) / 1e12
|
||||||
|
```
|
||||||
|
|
||||||
|
Average estimated memory bandwidth in GB/s:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
(rate(sglang:estimated_read_bytes_per_gpu_total[1m]) +
|
||||||
|
rate(sglang:estimated_write_bytes_per_gpu_total[1m])) / 1e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- These metrics are estimates intended for observability and trend analysis.
|
||||||
|
- Estimated memory bytes reflect modeled traffic and are not a direct hardware
|
||||||
|
counter from GPU profilers.
|
||||||
|
|||||||
@@ -702,6 +702,30 @@ class SchedulerMetricsCollector:
|
|||||||
),
|
),
|
||||||
labelnames=list(labels.keys()) + ["category"],
|
labelnames=list(labels.keys()) + ["category"],
|
||||||
)
|
)
|
||||||
|
self.estimated_flops_per_gpu_total = Counter(
|
||||||
|
name="sglang:estimated_flops_per_gpu_total",
|
||||||
|
documentation=(
|
||||||
|
"Estimated number of floating point operations per GPU "
|
||||||
|
"(for Model FLOPs Utilization calculations)."
|
||||||
|
),
|
||||||
|
labelnames=labels.keys(),
|
||||||
|
)
|
||||||
|
self.estimated_read_bytes_per_gpu_total = Counter(
|
||||||
|
name="sglang:estimated_read_bytes_per_gpu_total",
|
||||||
|
documentation=(
|
||||||
|
"Estimated number of bytes read from memory per GPU "
|
||||||
|
"(for Model FLOPs Utilization calculations)."
|
||||||
|
),
|
||||||
|
labelnames=labels.keys(),
|
||||||
|
)
|
||||||
|
self.estimated_write_bytes_per_gpu_total = Counter(
|
||||||
|
name="sglang:estimated_write_bytes_per_gpu_total",
|
||||||
|
documentation=(
|
||||||
|
"Estimated number of bytes written to memory per GPU "
|
||||||
|
"(for Model FLOPs Utilization calculations)."
|
||||||
|
),
|
||||||
|
labelnames=labels.keys(),
|
||||||
|
)
|
||||||
|
|
||||||
self.dp_cooperation_realtime_tokens_total = Counter(
|
self.dp_cooperation_realtime_tokens_total = Counter(
|
||||||
name="sglang:dp_cooperation_realtime_tokens_total",
|
name="sglang:dp_cooperation_realtime_tokens_total",
|
||||||
@@ -928,6 +952,25 @@ class SchedulerMetricsCollector:
|
|||||||
**dp_cooperation_info.to_labels(),
|
**dp_cooperation_info.to_labels(),
|
||||||
).inc(t)
|
).inc(t)
|
||||||
|
|
||||||
|
def increment_estimated_perf(
|
||||||
|
self,
|
||||||
|
num_flops_per_gpu: float = 0.0,
|
||||||
|
num_read_bytes_per_gpu: float = 0.0,
|
||||||
|
num_write_bytes_per_gpu: float = 0.0,
|
||||||
|
) -> None:
|
||||||
|
if num_flops_per_gpu > 0:
|
||||||
|
self.estimated_flops_per_gpu_total.labels(**self.labels).inc(
|
||||||
|
num_flops_per_gpu
|
||||||
|
)
|
||||||
|
if num_read_bytes_per_gpu > 0:
|
||||||
|
self.estimated_read_bytes_per_gpu_total.labels(**self.labels).inc(
|
||||||
|
num_read_bytes_per_gpu
|
||||||
|
)
|
||||||
|
if num_write_bytes_per_gpu > 0:
|
||||||
|
self.estimated_write_bytes_per_gpu_total.labels(**self.labels).inc(
|
||||||
|
num_write_bytes_per_gpu
|
||||||
|
)
|
||||||
|
|
||||||
def log_stats(self, stats: SchedulerStats) -> None:
|
def log_stats(self, stats: SchedulerStats) -> None:
|
||||||
self._log_gauge_queue_count(self.num_running_reqs, stats.num_running_reqs)
|
self._log_gauge_queue_count(self.num_running_reqs, stats.num_running_reqs)
|
||||||
self._log_gauge(self.num_used_tokens, stats.num_used_tokens)
|
self._log_gauge(self.num_used_tokens, stats.num_used_tokens)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from sglang.srt.disaggregation.kv_events import EventPublisherFactory, KVEventBatch
|
from sglang.srt.disaggregation.kv_events import EventPublisherFactory, KVEventBatch
|
||||||
from sglang.srt.disaggregation.utils import DisaggregationMode
|
from sglang.srt.disaggregation.utils import DisaggregationMode
|
||||||
@@ -114,6 +114,7 @@ class SchedulerMetricsMixin:
|
|||||||
self.stats = SchedulerStats()
|
self.stats = SchedulerStats()
|
||||||
|
|
||||||
# Metrics
|
# Metrics
|
||||||
|
self.enable_mfu_metrics = False
|
||||||
self.enable_metrics = self.server_args.enable_metrics
|
self.enable_metrics = self.server_args.enable_metrics
|
||||||
self.is_stats_logging_rank = self.attn_tp_rank == 0
|
self.is_stats_logging_rank = self.attn_tp_rank == 0
|
||||||
self.current_scheduler_metrics_enabled = self.enable_metrics and (
|
self.current_scheduler_metrics_enabled = self.enable_metrics and (
|
||||||
@@ -148,6 +149,12 @@ class SchedulerMetricsMixin:
|
|||||||
enable_hierarchical_cache=self.enable_hierarchical_cache,
|
enable_hierarchical_cache=self.enable_hierarchical_cache,
|
||||||
server_args=self.server_args,
|
server_args=self.server_args,
|
||||||
)
|
)
|
||||||
|
self.enable_mfu_metrics = bool(self.server_args.enable_mfu_metrics)
|
||||||
|
if self.enable_mfu_metrics:
|
||||||
|
self._init_estimated_perf_constants()
|
||||||
|
self._mfu_log_flops = 0.0
|
||||||
|
self._mfu_log_read_bytes = 0.0
|
||||||
|
self._mfu_log_write_bytes = 0.0
|
||||||
|
|
||||||
if ENABLE_METRICS_DEVICE_TIMER:
|
if ENABLE_METRICS_DEVICE_TIMER:
|
||||||
self.forward_pass_device_timer = DeviceTimer(
|
self.forward_pass_device_timer = DeviceTimer(
|
||||||
@@ -175,6 +182,139 @@ class SchedulerMetricsMixin:
|
|||||||
self.spec_num_forward_ct += bs
|
self.spec_num_forward_ct += bs
|
||||||
self.num_generated_tokens += num_accepted_tokens
|
self.num_generated_tokens += num_accepted_tokens
|
||||||
|
|
||||||
|
def _init_estimated_perf_constants(self: Scheduler) -> None:
|
||||||
|
model_config = self.model_config
|
||||||
|
hf_text_config = model_config.hf_text_config
|
||||||
|
|
||||||
|
hidden_size = float(model_config.hidden_size)
|
||||||
|
num_layers = float(getattr(model_config, "num_attention_layers", 0))
|
||||||
|
head_dim = float(getattr(model_config, "head_dim", 0))
|
||||||
|
num_attn_heads = float(model_config.get_num_attention_heads(self.tp_size))
|
||||||
|
num_kv_heads = float(model_config.get_num_kv_heads(self.tp_size))
|
||||||
|
intermediate_size = getattr(hf_text_config, "intermediate_size", None)
|
||||||
|
if intermediate_size is None:
|
||||||
|
intermediate_size = getattr(hf_text_config, "ffn_hidden_size", 0)
|
||||||
|
intermediate_size = float(intermediate_size)
|
||||||
|
|
||||||
|
dtype_num_bytes = getattr(model_config.dtype, "itemsize", None)
|
||||||
|
if dtype_num_bytes is None:
|
||||||
|
dtype_num_bytes = 2
|
||||||
|
# Keep this estimator lightweight and consistent with current server dtype.
|
||||||
|
# KV cache quantization-aware bytes can be added in a follow-up.
|
||||||
|
act_bytes = float(dtype_num_bytes)
|
||||||
|
w_bytes = float(dtype_num_bytes)
|
||||||
|
cache_bytes = float(dtype_num_bytes)
|
||||||
|
|
||||||
|
# Linear-layer FLOPs per token on one GPU.
|
||||||
|
attn_linear_flops = (
|
||||||
|
2.0 * hidden_size * head_dim * (num_attn_heads + 2.0 * num_kv_heads)
|
||||||
|
+ 2.0 * hidden_size * head_dim * num_attn_heads
|
||||||
|
)
|
||||||
|
mlp_flops = (
|
||||||
|
6.0 * hidden_size * intermediate_size if intermediate_size > 0 else 0.0
|
||||||
|
)
|
||||||
|
self._linear_flops_per_token = max(
|
||||||
|
0.0, (attn_linear_flops + mlp_flops) * num_layers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attention dot-product FLOPs coefficient to multiply token-context product.
|
||||||
|
# attn_qk + attn_av = 4 * q * TC * d * L
|
||||||
|
self._attn_dot_flops_coeff = 4.0 * num_attn_heads * head_dim * num_layers
|
||||||
|
|
||||||
|
# KV cache bytes (write one K and one V vector per generated token).
|
||||||
|
self._kv_cache_bytes_per_token = (
|
||||||
|
2.0 * num_layers * num_kv_heads * head_dim * cache_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
# Weight read bytes per token.
|
||||||
|
self._weight_read_bytes_per_token = (
|
||||||
|
hidden_size
|
||||||
|
* head_dim
|
||||||
|
* (num_attn_heads + 2.0 * num_kv_heads)
|
||||||
|
* w_bytes
|
||||||
|
* num_layers
|
||||||
|
+ hidden_size * head_dim * num_attn_heads * w_bytes * num_layers
|
||||||
|
+ (
|
||||||
|
3.0 * hidden_size * intermediate_size * w_bytes * num_layers
|
||||||
|
if intermediate_size > 0
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Activation movement bytes per token (coarse approximation).
|
||||||
|
self._qkv_act_bytes_per_token = (
|
||||||
|
hidden_size * act_bytes * num_layers
|
||||||
|
+ (num_attn_heads + 2.0 * num_kv_heads) * head_dim * act_bytes * num_layers
|
||||||
|
+ head_dim * num_attn_heads * act_bytes * num_layers
|
||||||
|
+ hidden_size * act_bytes * num_layers
|
||||||
|
)
|
||||||
|
self._ffn_act_bytes_per_token = (
|
||||||
|
3.0 * intermediate_size * act_bytes * num_layers
|
||||||
|
if intermediate_size > 0
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prefill reads Q/K/V activations from on-device memory.
|
||||||
|
self._prefill_attn_act_read_per_token = (
|
||||||
|
(num_attn_heads + 2.0 * num_kv_heads) * head_dim * act_bytes * num_layers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Decode reads Q from activation memory; K/V reads are from KV cache.
|
||||||
|
self._decode_q_read_bytes_per_token = (
|
||||||
|
num_attn_heads * head_dim * act_bytes * num_layers
|
||||||
|
)
|
||||||
|
|
||||||
|
def _estimate_prefill_perf(
|
||||||
|
self: Scheduler, num_tokens: int
|
||||||
|
) -> Tuple[float, float, float]:
|
||||||
|
tokens = max(0, int(num_tokens))
|
||||||
|
if tokens == 0:
|
||||||
|
return 0.0, 0.0, 0.0
|
||||||
|
|
||||||
|
# Causal prefill token-context product.
|
||||||
|
context_product = tokens * (tokens + 1) / 2.0
|
||||||
|
flops = (
|
||||||
|
tokens * self._linear_flops_per_token
|
||||||
|
+ self._attn_dot_flops_coeff * context_product
|
||||||
|
)
|
||||||
|
|
||||||
|
read_bytes = (
|
||||||
|
tokens * self._weight_read_bytes_per_token
|
||||||
|
+ tokens * self._qkv_act_bytes_per_token
|
||||||
|
+ tokens * self._prefill_attn_act_read_per_token
|
||||||
|
)
|
||||||
|
write_bytes = (
|
||||||
|
tokens * self._kv_cache_bytes_per_token
|
||||||
|
+ tokens * self._qkv_act_bytes_per_token
|
||||||
|
+ tokens * self._ffn_act_bytes_per_token
|
||||||
|
)
|
||||||
|
return flops, read_bytes, write_bytes
|
||||||
|
|
||||||
|
def _estimate_decode_perf(
|
||||||
|
self: Scheduler, batch: ScheduleBatch, num_tokens: int
|
||||||
|
) -> Tuple[float, float, float]:
|
||||||
|
tokens = max(0, int(num_tokens))
|
||||||
|
if tokens == 0:
|
||||||
|
return 0.0, 0.0, 0.0
|
||||||
|
|
||||||
|
total_context = float(batch.seq_lens_cpu.sum().item())
|
||||||
|
flops = (
|
||||||
|
tokens * self._linear_flops_per_token
|
||||||
|
+ self._attn_dot_flops_coeff * total_context
|
||||||
|
)
|
||||||
|
read_bytes = (
|
||||||
|
tokens * self._weight_read_bytes_per_token
|
||||||
|
+ tokens * self._qkv_act_bytes_per_token
|
||||||
|
+ tokens * self._decode_q_read_bytes_per_token
|
||||||
|
+ total_context * self._kv_cache_bytes_per_token
|
||||||
|
)
|
||||||
|
write_bytes = (
|
||||||
|
tokens * self._kv_cache_bytes_per_token
|
||||||
|
+ tokens * self._qkv_act_bytes_per_token
|
||||||
|
+ tokens * self._ffn_act_bytes_per_token
|
||||||
|
)
|
||||||
|
return flops, read_bytes, write_bytes
|
||||||
|
|
||||||
def reset_metrics(self: Scheduler):
|
def reset_metrics(self: Scheduler):
|
||||||
self.forward_ct_decode = 0
|
self.forward_ct_decode = 0
|
||||||
self.num_generated_tokens = 0
|
self.num_generated_tokens = 0
|
||||||
@@ -275,6 +415,11 @@ class SchedulerMetricsMixin:
|
|||||||
msg += f"{graph_backend[self.device]}: {can_run_cuda_graph}, "
|
msg += f"{graph_backend[self.device]}: {can_run_cuda_graph}, "
|
||||||
msg += f"input throughput (token/s): {self.last_input_throughput:.2f}"
|
msg += f"input throughput (token/s): {self.last_input_throughput:.2f}"
|
||||||
|
|
||||||
|
if self.enable_mfu_metrics and gap_latency > 0:
|
||||||
|
flops, _, _ = self._estimate_prefill_perf(prefill_stats.log_input_tokens)
|
||||||
|
tflops_per_s = flops / gap_latency / 1e12
|
||||||
|
msg += f", est. prefill TFLOPS/s (per GPU): {tflops_per_s:.2f}"
|
||||||
|
|
||||||
if self.is_stats_logging_rank:
|
if self.is_stats_logging_rank:
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
|
|
||||||
@@ -287,6 +432,15 @@ class SchedulerMetricsMixin:
|
|||||||
prefill_cache_tokens=prefill_stats.log_hit_tokens,
|
prefill_cache_tokens=prefill_stats.log_hit_tokens,
|
||||||
dp_cooperation_info=dp_cooperation_info,
|
dp_cooperation_info=dp_cooperation_info,
|
||||||
)
|
)
|
||||||
|
if self.enable_mfu_metrics:
|
||||||
|
flops, read_bytes, write_bytes = self._estimate_prefill_perf(
|
||||||
|
prefill_stats.log_input_tokens
|
||||||
|
)
|
||||||
|
self.metrics_collector.increment_estimated_perf(
|
||||||
|
num_flops_per_gpu=flops,
|
||||||
|
num_read_bytes_per_gpu=read_bytes,
|
||||||
|
num_write_bytes_per_gpu=write_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
# Basics
|
# Basics
|
||||||
total_tokens = prefill_stats.log_input_tokens + prefill_stats.log_hit_tokens
|
total_tokens = prefill_stats.log_input_tokens + prefill_stats.log_hit_tokens
|
||||||
@@ -354,11 +508,24 @@ class SchedulerMetricsMixin:
|
|||||||
|
|
||||||
# Every-iteration work: realtime token counting + status logger
|
# Every-iteration work: realtime token counting + status logger
|
||||||
if self.current_scheduler_metrics_enabled:
|
if self.current_scheduler_metrics_enabled:
|
||||||
|
decode_tokens = batch.batch_size() + num_accepted_tokens
|
||||||
self.metrics_collector.increment_realtime_tokens(
|
self.metrics_collector.increment_realtime_tokens(
|
||||||
# TODO unify this w/ the bumping logic in `Scheduler.num_generated_tokens` accumulator
|
# TODO unify this w/ the bumping logic in `Scheduler.num_generated_tokens` accumulator
|
||||||
decode_tokens=batch.batch_size() + num_accepted_tokens,
|
decode_tokens=decode_tokens,
|
||||||
dp_cooperation_info=batch.dp_cooperation_info,
|
dp_cooperation_info=batch.dp_cooperation_info,
|
||||||
)
|
)
|
||||||
|
if self.enable_mfu_metrics:
|
||||||
|
flops, read_bytes, write_bytes = self._estimate_decode_perf(
|
||||||
|
batch, decode_tokens
|
||||||
|
)
|
||||||
|
self.metrics_collector.increment_estimated_perf(
|
||||||
|
num_flops_per_gpu=flops,
|
||||||
|
num_read_bytes_per_gpu=read_bytes,
|
||||||
|
num_write_bytes_per_gpu=write_bytes,
|
||||||
|
)
|
||||||
|
self._mfu_log_flops += flops
|
||||||
|
self._mfu_log_read_bytes += read_bytes
|
||||||
|
self._mfu_log_write_bytes += write_bytes
|
||||||
|
|
||||||
if x := self.scheduler_status_logger:
|
if x := self.scheduler_status_logger:
|
||||||
x.maybe_dump(batch, self.waiting_queue)
|
x.maybe_dump(batch, self.waiting_queue)
|
||||||
@@ -490,6 +657,22 @@ class SchedulerMetricsMixin:
|
|||||||
f"#queue-req: {len(self.waiting_queue)}"
|
f"#queue-req: {len(self.waiting_queue)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.enable_mfu_metrics and gap_latency > 0:
|
||||||
|
flops_per_s = self._mfu_log_flops / gap_latency
|
||||||
|
read_bytes_per_s = self._mfu_log_read_bytes / gap_latency
|
||||||
|
write_bytes_per_s = self._mfu_log_write_bytes / gap_latency
|
||||||
|
tflops_per_s = flops_per_s / 1e12
|
||||||
|
read_gb_per_s = read_bytes_per_s / 1e9
|
||||||
|
write_gb_per_s = write_bytes_per_s / 1e9
|
||||||
|
msg += (
|
||||||
|
f", est. decode TFLOPS/s (per GPU): {tflops_per_s:.2f}, "
|
||||||
|
f"est. read BW (GB/s per GPU): {read_gb_per_s:.2f}, "
|
||||||
|
f"est. write BW (GB/s per GPU): {write_gb_per_s:.2f}"
|
||||||
|
)
|
||||||
|
self._mfu_log_flops = 0.0
|
||||||
|
self._mfu_log_read_bytes = 0.0
|
||||||
|
self._mfu_log_write_bytes = 0.0
|
||||||
|
|
||||||
if self.is_stats_logging_rank:
|
if self.is_stats_logging_rank:
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
if self.current_scheduler_metrics_enabled:
|
if self.current_scheduler_metrics_enabled:
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ class ServerArgs:
|
|||||||
crash_dump_folder: Optional[str] = None
|
crash_dump_folder: Optional[str] = None
|
||||||
show_time_cost: bool = False
|
show_time_cost: bool = False
|
||||||
enable_metrics: bool = False
|
enable_metrics: bool = False
|
||||||
|
enable_mfu_metrics: bool = False
|
||||||
enable_metrics_for_all_schedulers: bool = False
|
enable_metrics_for_all_schedulers: bool = False
|
||||||
tokenizer_metrics_custom_labels_header: str = "x-custom-labels"
|
tokenizer_metrics_custom_labels_header: str = "x-custom-labels"
|
||||||
tokenizer_metrics_allowed_custom_labels: Optional[List[str]] = None
|
tokenizer_metrics_allowed_custom_labels: Optional[List[str]] = None
|
||||||
@@ -4220,6 +4221,11 @@ class ServerArgs:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enable log prometheus metrics.",
|
help="Enable log prometheus metrics.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--enable-mfu-metrics",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable estimated MFU-related prometheus metrics.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--enable-metrics-for-all-schedulers",
|
"--enable-metrics-for-all-schedulers",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ class TestEnableMetrics(CustomTestCase):
|
|||||||
self._execute_core(
|
self._execute_core(
|
||||||
other_args=[],
|
other_args=[],
|
||||||
verify_metrics_extra=None,
|
verify_metrics_extra=None,
|
||||||
|
expect_mfu_metrics=True,
|
||||||
|
enable_mfu_metrics=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mfu_metrics_gate_disabled(self):
|
||||||
|
"""MFU metrics should not be emitted when the gate is disabled."""
|
||||||
|
self._execute_core(
|
||||||
|
other_args=[],
|
||||||
|
verify_metrics_extra=None,
|
||||||
|
expect_mfu_metrics=False,
|
||||||
|
enable_mfu_metrics=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_metrics_2gpu(self):
|
def test_metrics_2gpu(self):
|
||||||
@@ -71,19 +82,30 @@ class TestEnableMetrics(CustomTestCase):
|
|||||||
self._execute_core(
|
self._execute_core(
|
||||||
other_args=["--tp", "2", "--dp", "2", "--enable-dp-attention"],
|
other_args=["--tp", "2", "--dp", "2", "--enable-dp-attention"],
|
||||||
verify_metrics_extra=_verify_metrics_extra,
|
verify_metrics_extra=_verify_metrics_extra,
|
||||||
|
expect_mfu_metrics=True,
|
||||||
|
enable_mfu_metrics=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _execute_core(self, other_args, verify_metrics_extra):
|
def _execute_core(
|
||||||
|
self,
|
||||||
|
other_args,
|
||||||
|
verify_metrics_extra,
|
||||||
|
expect_mfu_metrics: bool,
|
||||||
|
enable_mfu_metrics: bool,
|
||||||
|
):
|
||||||
with (
|
with (
|
||||||
envs.SGLANG_ENABLE_METRICS_DP_ATTENTION.override(True),
|
envs.SGLANG_ENABLE_METRICS_DP_ATTENTION.override(True),
|
||||||
envs.SGLANG_ENABLE_METRICS_DEVICE_TIMER.override(True),
|
envs.SGLANG_ENABLE_METRICS_DEVICE_TIMER.override(True),
|
||||||
envs.SGLANG_TEST_RETRACT.override(True),
|
envs.SGLANG_TEST_RETRACT.override(True),
|
||||||
):
|
):
|
||||||
|
launch_args = ["--enable-metrics", "--cuda-graph-max-bs", 2, *other_args]
|
||||||
|
if enable_mfu_metrics:
|
||||||
|
launch_args.insert(1, "--enable-mfu-metrics")
|
||||||
process = popen_launch_server(
|
process = popen_launch_server(
|
||||||
_MODEL_NAME,
|
_MODEL_NAME,
|
||||||
DEFAULT_URL_FOR_TEST,
|
DEFAULT_URL_FOR_TEST,
|
||||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
other_args=["--enable-metrics", "--cuda-graph-max-bs", 2, *other_args],
|
other_args=launch_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -125,13 +147,13 @@ class TestEnableMetrics(CustomTestCase):
|
|||||||
print(f"metrics_text=\n{metrics_text}")
|
print(f"metrics_text=\n{metrics_text}")
|
||||||
|
|
||||||
metrics = _parse_prometheus_metrics(metrics_text)
|
metrics = _parse_prometheus_metrics(metrics_text)
|
||||||
self._verify_metrics_common(metrics_text, metrics)
|
self._verify_metrics_common(metrics_text, metrics, expect_mfu_metrics)
|
||||||
if verify_metrics_extra is not None:
|
if verify_metrics_extra is not None:
|
||||||
verify_metrics_extra(metrics)
|
verify_metrics_extra(metrics)
|
||||||
finally:
|
finally:
|
||||||
kill_process_tree(process.pid)
|
kill_process_tree(process.pid)
|
||||||
|
|
||||||
def _verify_metrics_common(self, metrics_text, metrics):
|
def _verify_metrics_common(self, metrics_text, metrics, expect_mfu_metrics: bool):
|
||||||
essential_metrics = [
|
essential_metrics = [
|
||||||
"sglang:num_running_reqs",
|
"sglang:num_running_reqs",
|
||||||
"sglang:num_used_tokens",
|
"sglang:num_used_tokens",
|
||||||
@@ -154,6 +176,13 @@ class TestEnableMetrics(CustomTestCase):
|
|||||||
"sglang:routing_key_running_req_count",
|
"sglang:routing_key_running_req_count",
|
||||||
"sglang:routing_key_all_req_count",
|
"sglang:routing_key_all_req_count",
|
||||||
]
|
]
|
||||||
|
mfu_metrics = [
|
||||||
|
"sglang:estimated_flops_per_gpu_total",
|
||||||
|
"sglang:estimated_read_bytes_per_gpu_total",
|
||||||
|
"sglang:estimated_write_bytes_per_gpu_total",
|
||||||
|
]
|
||||||
|
if expect_mfu_metrics:
|
||||||
|
essential_metrics.extend(mfu_metrics)
|
||||||
for metric in essential_metrics:
|
for metric in essential_metrics:
|
||||||
self.assertIn(metric, metrics_text, f"Missing metric: {metric}")
|
self.assertIn(metric, metrics_text, f"Missing metric: {metric}")
|
||||||
|
|
||||||
@@ -186,6 +215,39 @@ class TestEnableMetrics(CustomTestCase):
|
|||||||
]
|
]
|
||||||
_check_metrics_positive(self, metrics, metrics_to_check)
|
_check_metrics_positive(self, metrics, metrics_to_check)
|
||||||
|
|
||||||
|
if expect_mfu_metrics:
|
||||||
|
# Estimated perf metrics may have multiple series (e.g., by rank). Ensure
|
||||||
|
# that at least one series for this model has a positive accumulated value.
|
||||||
|
for metric_name in mfu_metrics:
|
||||||
|
values = [
|
||||||
|
sample.value
|
||||||
|
for sample in metrics.get(metric_name, [])
|
||||||
|
if sample.labels.get("model_name") == _MODEL_NAME
|
||||||
|
]
|
||||||
|
self.assertTrue(
|
||||||
|
values, f"{metric_name}: no samples for model {_MODEL_NAME}"
|
||||||
|
)
|
||||||
|
self.assertGreater(
|
||||||
|
sum(values),
|
||||||
|
0,
|
||||||
|
f"{metric_name}: expected positive total for model {_MODEL_NAME}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# With only --enable-metrics (without --enable-mfu-metrics), MFU
|
||||||
|
# counters should not emit positive values.
|
||||||
|
for metric_name in mfu_metrics:
|
||||||
|
values = [
|
||||||
|
sample.value
|
||||||
|
for sample in metrics.get(metric_name, [])
|
||||||
|
if sample.labels.get("model_name") == _MODEL_NAME
|
||||||
|
]
|
||||||
|
if values:
|
||||||
|
self.assertEqual(
|
||||||
|
sum(values),
|
||||||
|
0,
|
||||||
|
f"{metric_name}: expected no positive samples with MFU metrics gate disabled",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_prometheus_metrics(metrics_text: str) -> Dict[str, List[Sample]]:
|
def _parse_prometheus_metrics(metrics_text: str) -> Dict[str, List[Sample]]:
|
||||||
result = {}
|
result = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user