kvtop 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kvtop-0.1.0/Cargo.lock +7 -0
- kvtop-0.1.0/Cargo.toml +20 -0
- kvtop-0.1.0/LICENSE +21 -0
- kvtop-0.1.0/PKG-INFO +55 -0
- kvtop-0.1.0/PUBLISH.md +66 -0
- kvtop-0.1.0/README.md +42 -0
- kvtop-0.1.0/examples/vllm.prom +11 -0
- kvtop-0.1.0/pyproject.toml +20 -0
- kvtop-0.1.0/src/main.rs +207 -0
kvtop-0.1.0/Cargo.lock
ADDED
kvtop-0.1.0/Cargo.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "kvtop"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
rust-version = "1.74"
|
|
6
|
+
description = "Read-only CLI for inspecting KV-cache metrics from Prometheus snapshots"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["llm", "kv-cache", "observability", "cli", "prometheus"]
|
|
10
|
+
categories = ["command-line-utilities", "development-tools::profiling"]
|
|
11
|
+
include = ["src/**", "Cargo.toml", "Cargo.lock", "README.md", "PUBLISH.md", "LICENSE", "examples/**", "pyproject.toml"]
|
|
12
|
+
|
|
13
|
+
[[bin]]
|
|
14
|
+
name = "kvtop"
|
|
15
|
+
path = "src/main.rs"
|
|
16
|
+
|
|
17
|
+
[profile.release]
|
|
18
|
+
lto = true
|
|
19
|
+
strip = true
|
|
20
|
+
codegen-units = 1
|
kvtop-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kvtop contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kvtop-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kvtop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Programming Language :: Rust
|
|
7
|
+
Classifier: Topic :: System :: Monitoring
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Summary: Read-only Rust CLI for inspecting KV-cache metrics from Prometheus snapshots
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
12
|
+
|
|
13
|
+
# kvtop
|
|
14
|
+
|
|
15
|
+
A small, read-only Rust CLI for inspecting KV-cache metrics from Prometheus text snapshots.
|
|
16
|
+
|
|
17
|
+
Version 0.1.0 reads a UTF-8 file or stdin, selects KV-related metrics, and emits a terminal table or JSON. It is useful with a serving engine's existing metrics endpoint; it does not attach to CUDA or modify a running engine. The Rust executable has no third-party crate dependencies.
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
curl -fsS http://localhost:8000/metrics | kvtop
|
|
23
|
+
kvtop --file snapshot.prom
|
|
24
|
+
kvtop --file snapshot.prom --json
|
|
25
|
+
kvtop --file snapshot.prom --match prefix_cache
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Default name filters cover `kv_cache`, `prefix_cache`, `kv_block`, `kv_transfer`, `cached_tokens`, `prompt_tokens_cached`, and `prefill_kv_computed`. `--match` replaces those filters and can be repeated. The input limit is 16 MiB. Empty or missing measurements return an error instead of zero.
|
|
29
|
+
|
|
30
|
+
Values retain the producer's units and labels. In particular, vLLM's `kv_cache_usage_perc` uses 1 for 100%, and prefix-cache counters count tokens, not requests. Samples from different workers are kept separate. This tool does not infer physical bytes from generic GPU usage, derive interval hit rates from cumulative counters, or claim a causal performance saving.
|
|
31
|
+
|
|
32
|
+
JSON has `schema_version: 1` and a `samples` array. Each sample contains `name`, a raw Prometheus `labels` string, `value`, and `raw_value`. Nonfinite values have JSON `null` plus their explicit `raw_value`; very large numeric values may lose precision in `value`, so use `raw_value` when exact representation matters. Timestamps and exemplars are not included in this first version. This is a metric-snapshot inspector, not a general Prometheus validator.
|
|
33
|
+
|
|
34
|
+
## Build and test
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
cargo test
|
|
38
|
+
cargo build --release
|
|
39
|
+
cargo run -- --file examples/vllm.prom --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The fixture is synthetic. An actual engine integration and real GPU performance measurements have not been validated by this release preparation.
|
|
43
|
+
|
|
44
|
+
## Distribution
|
|
45
|
+
|
|
46
|
+
The package is configured for Cargo and for a Python wheel containing the same Rust executable, using Maturin's `bin` mode. Publishing preparation is not evidence that either registry currently hosts this project. See `PUBLISH.md` in the source delivery for the commands and publication status.
|
|
47
|
+
|
|
48
|
+
## Existing command with the same name
|
|
49
|
+
|
|
50
|
+
This is an independent project, unaffiliated with `ovg-project/kvcached`, which already distributes a Python command named `kvtop`. Their command inspects kvcached shared-memory records; this program reads Prometheus snapshots. Installing both commands into one environment may overwrite or shadow one entrypoint. Use a separate virtual environment or an explicit executable path. This project does not replace, patch, or depend on kvcached.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT.
|
|
55
|
+
|
kvtop-0.1.0/PUBLISH.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Publication handoff
|
|
2
|
+
|
|
3
|
+
Prepared on 2026-09-22. Nothing has been uploaded to PyPI or crates.io.
|
|
4
|
+
|
|
5
|
+
Public registry checks at preparation time:
|
|
6
|
+
- `https://pypi.org/pypi/kvtop/json`: HTTP 404, `Not Found`.
|
|
7
|
+
- `https://pypi.org/simple/kvtop/`: HTTP 404.
|
|
8
|
+
- `https://crates.io/api/v1/crates/kvtop`: HTTP 404, `crate kvtop does not exist`.
|
|
9
|
+
- `https://index.crates.io/kv/to/kvtop`: HTTP 404.
|
|
10
|
+
|
|
11
|
+
Absence from public endpoints does not guarantee first-upload eligibility: a registry can reserve or prohibit a name, and its state can change.
|
|
12
|
+
|
|
13
|
+
The preparation environment did not contain Cargo or PyPI publishing credentials. Never paste an API token into chat or commit it to this source tree.
|
|
14
|
+
|
|
15
|
+
## Verify the release
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
cargo test
|
|
19
|
+
cargo package --list
|
|
20
|
+
cargo publish --dry-run
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## crates.io
|
|
24
|
+
|
|
25
|
+
On your authenticated machine, `cargo login` prompts for your own crates.io publishing token. Then:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
cargo publish
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The crate contains a binary named `kvtop`. Check the published project and use a fresh install to verify it:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
cargo install kvtop --version 0.1.0 --locked
|
|
35
|
+
kvtop --version
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## PyPI
|
|
39
|
+
|
|
40
|
+
Install Maturin and Twine in a release environment. Build the same Rust binary into a wheel:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
python -m pip install 'maturin>=1.8,<2' twine
|
|
44
|
+
maturin build --release --bindings bin --compatibility pypi --out dist
|
|
45
|
+
maturin sdist --out dist
|
|
46
|
+
python -m twine check dist/*
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Publish using your locally configured PyPI authentication, Trusted Publishing, or Twine's interactive token prompt. For the token prompt, the username is `__token__` and the password is your API token:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
python -m twine upload dist/*
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
First release: a Linux x86_64 wheel and source distribution. Other operating systems require their own wheels or a Rust toolchain for source builds. Do not advertise prebuilt macOS/Windows support until those builds exist.
|
|
56
|
+
|
|
57
|
+
In a fresh environment, verify installation and functionality:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
python -m venv /tmp/kvtop-release-check
|
|
61
|
+
/tmp/kvtop-release-check/bin/python -m pip install kvtop==0.1.0
|
|
62
|
+
/tmp/kvtop-release-check/bin/kvtop --version
|
|
63
|
+
/tmp/kvtop-release-check/bin/kvtop --file examples/vllm.prom --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Keep this installation separate from kvcached to avoid its existing `kvtop` entrypoint.
|
kvtop-0.1.0/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# kvtop
|
|
2
|
+
|
|
3
|
+
A small, read-only Rust CLI for inspecting KV-cache metrics from Prometheus text snapshots.
|
|
4
|
+
|
|
5
|
+
Version 0.1.0 reads a UTF-8 file or stdin, selects KV-related metrics, and emits a terminal table or JSON. It is useful with a serving engine's existing metrics endpoint; it does not attach to CUDA or modify a running engine. The Rust executable has no third-party crate dependencies.
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
curl -fsS http://localhost:8000/metrics | kvtop
|
|
11
|
+
kvtop --file snapshot.prom
|
|
12
|
+
kvtop --file snapshot.prom --json
|
|
13
|
+
kvtop --file snapshot.prom --match prefix_cache
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Default name filters cover `kv_cache`, `prefix_cache`, `kv_block`, `kv_transfer`, `cached_tokens`, `prompt_tokens_cached`, and `prefill_kv_computed`. `--match` replaces those filters and can be repeated. The input limit is 16 MiB. Empty or missing measurements return an error instead of zero.
|
|
17
|
+
|
|
18
|
+
Values retain the producer's units and labels. In particular, vLLM's `kv_cache_usage_perc` uses 1 for 100%, and prefix-cache counters count tokens, not requests. Samples from different workers are kept separate. This tool does not infer physical bytes from generic GPU usage, derive interval hit rates from cumulative counters, or claim a causal performance saving.
|
|
19
|
+
|
|
20
|
+
JSON has `schema_version: 1` and a `samples` array. Each sample contains `name`, a raw Prometheus `labels` string, `value`, and `raw_value`. Nonfinite values have JSON `null` plus their explicit `raw_value`; very large numeric values may lose precision in `value`, so use `raw_value` when exact representation matters. Timestamps and exemplars are not included in this first version. This is a metric-snapshot inspector, not a general Prometheus validator.
|
|
21
|
+
|
|
22
|
+
## Build and test
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
cargo test
|
|
26
|
+
cargo build --release
|
|
27
|
+
cargo run -- --file examples/vllm.prom --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The fixture is synthetic. An actual engine integration and real GPU performance measurements have not been validated by this release preparation.
|
|
31
|
+
|
|
32
|
+
## Distribution
|
|
33
|
+
|
|
34
|
+
The package is configured for Cargo and for a Python wheel containing the same Rust executable, using Maturin's `bin` mode. Publishing preparation is not evidence that either registry currently hosts this project. See `PUBLISH.md` in the source delivery for the commands and publication status.
|
|
35
|
+
|
|
36
|
+
## Existing command with the same name
|
|
37
|
+
|
|
38
|
+
This is an independent project, unaffiliated with `ovg-project/kvcached`, which already distributes a Python command named `kvtop`. Their command inspects kvcached shared-memory records; this program reads Prometheus snapshots. Installing both commands into one environment may overwrite or shadow one entrypoint. Use a separate virtual environment or an explicit executable path. This project does not replace, patch, or depend on kvcached.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Synthetic fixture using vLLM metric names, not measurements from a real server.
|
|
2
|
+
# HELP vllm:kv_cache_usage_perc KV-cache usage. 1 means 100 percent usage.
|
|
3
|
+
# TYPE vllm:kv_cache_usage_perc gauge
|
|
4
|
+
vllm:kv_cache_usage_perc{model_name="example-model",engine="0"} 0.42
|
|
5
|
+
vllm:kv_cache_usage_perc{model_name="example-model",engine="1"} 0.17
|
|
6
|
+
# TYPE vllm:prefix_cache_queries_total counter
|
|
7
|
+
vllm:prefix_cache_queries_total{model_name="example-model"} 10000
|
|
8
|
+
# TYPE vllm:prefix_cache_hits_total counter
|
|
9
|
+
vllm:prefix_cache_hits_total{model_name="example-model"} 6000
|
|
10
|
+
vllm:request_prefill_kv_computed_tokens_sum{model_name="example-model"} 4000
|
|
11
|
+
vllm:num_requests_running{model_name="example-model"} 3
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.8,<2"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kvtop"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Read-only Rust CLI for inspecting KV-cache metrics from Prometheus snapshots"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Programming Language :: Rust",
|
|
16
|
+
"Topic :: System :: Monitoring",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[tool.maturin]
|
|
20
|
+
bindings = "bin"
|
kvtop-0.1.0/src/main.rs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
use std::env;
|
|
2
|
+
use std::fs::File;
|
|
3
|
+
use std::io::{self, Read, Write};
|
|
4
|
+
|
|
5
|
+
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
6
|
+
const MAX_BYTES: u64 = 16 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_PATTERNS: &[&str] = &[
|
|
8
|
+
"kv_cache", "prefix_cache", "kv_block", "kv_transfer", "cached_tokens",
|
|
9
|
+
"prompt_tokens_cached", "prefill_kv_computed",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, PartialEq)]
|
|
13
|
+
struct Sample {
|
|
14
|
+
name: String,
|
|
15
|
+
labels: String,
|
|
16
|
+
raw_value: String,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn parse_sample(line: &str) -> Result<Sample, String> {
|
|
20
|
+
let mut quoted = false;
|
|
21
|
+
let mut escaped = false;
|
|
22
|
+
let mut depth = 0u8;
|
|
23
|
+
let mut end = None;
|
|
24
|
+
for (i, c) in line.char_indices() {
|
|
25
|
+
if quoted {
|
|
26
|
+
if escaped { escaped = false; }
|
|
27
|
+
else if c == '\\' { escaped = true; }
|
|
28
|
+
else if c == '"' { quoted = false; }
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
match c {
|
|
32
|
+
'"' if depth == 1 => quoted = true,
|
|
33
|
+
'{' if depth == 0 => depth = 1,
|
|
34
|
+
'}' if depth == 1 => depth = 0,
|
|
35
|
+
'{' | '}' | '"' => return Err("invalid label delimiters".into()),
|
|
36
|
+
c if c.is_whitespace() && depth == 0 => { end = Some(i); break; }
|
|
37
|
+
_ => {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if quoted || depth != 0 { return Err("unterminated label set".into()); }
|
|
41
|
+
let end = end.ok_or("missing sample value")?;
|
|
42
|
+
let identity = &line[..end];
|
|
43
|
+
let (name, labels) = match identity.find('{') {
|
|
44
|
+
Some(i) if identity.ends_with('}') => (&identity[..i], &identity[i..]),
|
|
45
|
+
Some(_) => return Err("invalid sample identity".into()),
|
|
46
|
+
None => (identity, ""),
|
|
47
|
+
};
|
|
48
|
+
if name.is_empty() || !name.chars().enumerate().all(|(i, c)| {
|
|
49
|
+
c.is_ascii_alphabetic() || c == '_' || c == ':' || (i > 0 && c.is_ascii_digit())
|
|
50
|
+
}) { return Err("invalid metric name".into()); }
|
|
51
|
+
let raw_value = line[end..].split_whitespace().next().ok_or("missing sample value")?;
|
|
52
|
+
if !matches!(raw_value, "+Inf" | "-Inf" | "Inf" | "NaN") {
|
|
53
|
+
let value = raw_value.parse::<f64>().map_err(|_| "invalid numeric value")?;
|
|
54
|
+
if !value.is_finite() { return Err("numeric value overflow or invalid spelling".into()); }
|
|
55
|
+
}
|
|
56
|
+
Ok(Sample { name: name.into(), labels: labels.into(), raw_value: raw_value.into() })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn select(input: &str, patterns: &[String]) -> Result<Vec<Sample>, String> {
|
|
60
|
+
let mut samples = Vec::new();
|
|
61
|
+
for (index, line) in input.lines().enumerate() {
|
|
62
|
+
let line = line.trim();
|
|
63
|
+
if line.is_empty() || line.starts_with('#') { continue; }
|
|
64
|
+
let name = line.split(|c: char| c == '{' || c.is_whitespace()).next().unwrap_or("");
|
|
65
|
+
if !patterns.iter().any(|p| name.contains(p)) { continue; }
|
|
66
|
+
samples.push(parse_sample(line).map_err(|e| format!("line {}: {}", index + 1, e))?);
|
|
67
|
+
}
|
|
68
|
+
if samples.is_empty() {
|
|
69
|
+
return Err("no matching KV-cache metrics; check the input or supply --match SUBSTRING".into());
|
|
70
|
+
}
|
|
71
|
+
Ok(samples)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn json_string(value: &str) -> String {
|
|
75
|
+
let mut out = String::from("\"");
|
|
76
|
+
for c in value.chars() {
|
|
77
|
+
match c {
|
|
78
|
+
'"' => out.push_str("\\\""),
|
|
79
|
+
'\\' => out.push_str("\\\\"),
|
|
80
|
+
c if c <= '\u{1f}' => out.push_str(&format!("\\u{:04x}", c as u32)),
|
|
81
|
+
c => out.push(c),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
out.push('"');
|
|
85
|
+
out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn render(samples: &[Sample], json: bool) -> String {
|
|
89
|
+
if json {
|
|
90
|
+
let rows = samples.iter().map(|s| {
|
|
91
|
+
let value = s.raw_value.parse::<f64>().ok().filter(|v| v.is_finite())
|
|
92
|
+
.map(|v| v.to_string()).unwrap_or_else(|| "null".into());
|
|
93
|
+
format!("{{\"name\":{},\"labels\":{},\"value\":{},\"raw_value\":{}}}",
|
|
94
|
+
json_string(&s.name), json_string(&s.labels), value, json_string(&s.raw_value))
|
|
95
|
+
}).collect::<Vec<_>>().join(",");
|
|
96
|
+
format!("{{\"schema_version\":1,\"source_format\":\"prometheus\",\"samples\":[{}]}}\n", rows)
|
|
97
|
+
} else {
|
|
98
|
+
let mut out = String::from("KV/cache metric snapshot — raw producer units\nVALUE\tMETRIC\tLABELS\n");
|
|
99
|
+
for s in samples {
|
|
100
|
+
out.push_str(&format!("{}\t{}\t{}\n", s.raw_value, s.name, s.labels.escape_debug()));
|
|
101
|
+
}
|
|
102
|
+
out
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn usage() -> String {
|
|
107
|
+
format!("kvtop {VERSION} — read-only KV-cache metric inspector\n\n\
|
|
108
|
+
Usage: kvtop [--file PATH] [--json] [--match SUBSTRING]...\n\
|
|
109
|
+
curl -fsS http://localhost:8000/metrics | kvtop\n\n\
|
|
110
|
+
Reads a Prometheus text snapshot from stdin or a UTF-8 file (maximum 16 MiB).\n\
|
|
111
|
+
--match replaces the default KV-cache name filters; repeated filters use OR.\n\
|
|
112
|
+
Values retain producer units. Counters are cumulative, not interval hit rates.\n\
|
|
113
|
+
Missing data is an error, never a fabricated zero. No inference process is modified.\n\
|
|
114
|
+
This independent Rust application is not kvcached's Python kvtop command.\n")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn run(args: Vec<String>) -> Result<String, String> {
|
|
118
|
+
let mut file = None;
|
|
119
|
+
let mut json = false;
|
|
120
|
+
let mut patterns = Vec::new();
|
|
121
|
+
let mut args = args.into_iter();
|
|
122
|
+
while let Some(arg) = args.next() {
|
|
123
|
+
match arg.as_str() {
|
|
124
|
+
"--help" | "-h" => return Ok(usage()),
|
|
125
|
+
"--version" | "-V" => return Ok(format!("kvtop {VERSION}\n")),
|
|
126
|
+
"--json" => json = true,
|
|
127
|
+
"--file" => {
|
|
128
|
+
if file.is_some() { return Err("--file may only be supplied once".into()); }
|
|
129
|
+
file = Some(args.next().ok_or("--file requires a path")?);
|
|
130
|
+
}
|
|
131
|
+
"--match" => {
|
|
132
|
+
let value = args.next().ok_or("--match requires a substring")?;
|
|
133
|
+
if value.is_empty() { return Err("--match cannot be empty".into()); }
|
|
134
|
+
patterns.push(value);
|
|
135
|
+
}
|
|
136
|
+
_ => return Err(format!("unknown argument: {}; see --help", arg.escape_debug())),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if patterns.is_empty() { patterns = DEFAULT_PATTERNS.iter().map(|s| s.to_string()).collect(); }
|
|
140
|
+
let reader: Box<dyn Read> = match file {
|
|
141
|
+
Some(path) => Box::new(File::open(path).map_err(|e| format!("cannot open input: {e}"))?),
|
|
142
|
+
None => Box::new(io::stdin()),
|
|
143
|
+
};
|
|
144
|
+
let mut input = String::new();
|
|
145
|
+
reader.take(MAX_BYTES + 1).read_to_string(&mut input).map_err(|e| format!("cannot read UTF-8 input: {e}"))?;
|
|
146
|
+
if input.len() as u64 > MAX_BYTES { return Err("snapshot exceeds the 16 MiB limit".into()); }
|
|
147
|
+
Ok(render(&select(&input, &patterns)?, json))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn main() {
|
|
151
|
+
match run(env::args().skip(1).collect()) {
|
|
152
|
+
Ok(output) => {
|
|
153
|
+
if let Err(e) = io::stdout().lock().write_all(output.as_bytes()) {
|
|
154
|
+
if e.kind() != io::ErrorKind::BrokenPipe { eprintln!("kvtop: {e}"); std::process::exit(1); }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
Err(e) => { eprintln!("kvtop: {e}"); std::process::exit(2); }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[cfg(test)]
|
|
162
|
+
mod tests {
|
|
163
|
+
use super::*;
|
|
164
|
+
fn patterns() -> Vec<String> { DEFAULT_PATTERNS.iter().map(|s| s.to_string()).collect() }
|
|
165
|
+
#[test]
|
|
166
|
+
fn parses_realistic_snapshot_without_aggregating_replicas() {
|
|
167
|
+
let s = select(include_str!("../examples/vllm.prom"), &patterns()).unwrap();
|
|
168
|
+
assert_eq!(s.len(), 5);
|
|
169
|
+
assert_eq!(s[0].raw_value, "0.42");
|
|
170
|
+
assert_ne!(s[0].labels, s[1].labels);
|
|
171
|
+
assert!(!s.iter().any(|s| s.name.contains("requests_running")));
|
|
172
|
+
}
|
|
173
|
+
#[test]
|
|
174
|
+
fn quoted_spaces_braces_and_escapes_do_not_split_identity() {
|
|
175
|
+
let s = parse_sample(r#"kv_cache_bytes{model="a } \" b",replica="1"} 2e3 1234"#).unwrap();
|
|
176
|
+
assert_eq!(s.name, "kv_cache_bytes");
|
|
177
|
+
assert_eq!(s.raw_value, "2e3");
|
|
178
|
+
assert!(s.labels.contains("replica"));
|
|
179
|
+
}
|
|
180
|
+
#[test]
|
|
181
|
+
fn missing_metrics_and_bad_samples_are_errors() {
|
|
182
|
+
assert!(select("# HELP a example\nother_metric 0", &patterns()).is_err());
|
|
183
|
+
assert!(select("kv_cache_bytes nope", &patterns()).is_err());
|
|
184
|
+
assert!(parse_sample("kv_cache_bytes{broken=\"x} 1").is_err());
|
|
185
|
+
assert!(parse_sample("kv_cache_bytes 1e999").is_err());
|
|
186
|
+
}
|
|
187
|
+
#[test]
|
|
188
|
+
fn nonfinite_values_remain_explicit() {
|
|
189
|
+
let samples = ["NaN", "+Inf", "-Inf"].iter().map(|v| parse_sample(&format!("kv_cache_bytes {v}")).unwrap()).collect::<Vec<_>>();
|
|
190
|
+
let output = render(&samples, true);
|
|
191
|
+
assert_eq!(output.matches("\"value\":null").count(), 3);
|
|
192
|
+
assert!(output.contains("\"raw_value\":\"NaN\""));
|
|
193
|
+
}
|
|
194
|
+
#[test]
|
|
195
|
+
fn custom_filters_override_defaults() {
|
|
196
|
+
let s = select("kv_cache_bytes 1\ncustom_metric 2", &["custom_".into()]).unwrap();
|
|
197
|
+
assert_eq!(s.len(), 1);
|
|
198
|
+
assert_eq!(s[0].name, "custom_metric");
|
|
199
|
+
}
|
|
200
|
+
#[test]
|
|
201
|
+
fn escapes_json_and_rejects_unknown_cli_options() {
|
|
202
|
+
assert_eq!(json_string("x\n\"\\"), "\"x\\u000a\\\"\\\\\"");
|
|
203
|
+
assert!(run(vec!["--unknown".into()]).is_err());
|
|
204
|
+
assert!(run(vec!["--file".into()]).is_err());
|
|
205
|
+
assert!(run(vec!["--match".into(), "".into()]).is_err());
|
|
206
|
+
}
|
|
207
|
+
}
|