MFU metrics in Prometheus (#19395)

This commit is contained in:
Aishwarya Ramasethu
2026-03-29 23:40:06 -07:00
committed by GitHub
parent 1a4b383fac
commit c32ee48886
6 changed files with 338 additions and 7 deletions
@@ -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] |
+37 -1
View File
@@ -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:
+6
View File
@@ -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",
+66 -4
View File
@@ -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 = {}