Skip to content

HIP: on-device autotuning for gfx1100 decode (kernel shapes, draft and CPU settings) - #744

Closed
xyzzing wants to merge 261 commits into
Niko1221:mainfrom
xyzzing:hip-decode-autotune
Closed

xyzzing wants to merge 261 commits into
Niko1221:mainfrom
xyzzing:hip-decode-autotune

Conversation

@xyzzing

@xyzzing xyzzing commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Summary

On-device decode autotuning for the AMD HIP backend (gfx1100): per-shape rows-per-block tuning for the decode expert kernels and iq_mmvq (bitwise-exact variants only), a guarded tuning-table format, an on-device tuner, and an orchestrator that sweeps kernels, draft floor, CPU workers, and drafting windows with the user's own prompts. Settings-layer sweeps (--spec, --suffix-draft, --spec-min-p, workers) now work on HIP too, and calibrate is HIP-aware (keeps the config's --pcie-frac).

Both layers keep answers exactly the same: kernel variants are byte-compared on-device and end-to-end; the settings layer is greedy-verification-exact.

Measured on the target hardware (RX 7900 XTX 24 GB, IQ3_S, ROCm 7.1.1)

  • Kernel layer: the shipped defaults already win on every IQ3_S shape (deltas ≤1%, below the keep threshold) — an honest null; the sweep took ~1 min + 4 engine starts.
  • Settings layer: defaults fastest (57.0 tok/s on the tool's workload).
  • Draft floor: --spec-min-p 0.70 = 63.1 tok/s vs baseline (~+10%), token-identity verified end-to-end — adopted in our serving config.
  • 128K-context one-shot and ROCm-nightly comparisons are in progress on the same box; the tool's report (strata-<model>.autotune.json) carries every measurement.

Notes

  • Base is this fork's HIP integration main (4da9c59); a rebase onto current upstream main is queued (the files the patch touches are untouched by 0.1.36–0.1.38's kernel changes, so the rebase should be mechanical).
  • The free-VRAM pre-check over-counts (section 6.1 of the authoring notes) — known, fix queued.
  • gfx1100 measured; the table's arch gate keeps other cards on defaults until tuned.

pete and others added 30 commits September 26, 2026 19:20
…physical_cores() (Niko1221#14, fixes Niko1221#13 and Niko1221#11)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…ad of spinning (Niko1221#9, fixes Niko1221#4)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
… engine-queue race between requests (Niko1221#7)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…ts through the decode windows (Niko1221#8, Niko1221#10)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
… pool fix)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
START-HERE.bat on an installed setup went straight to starting the model, so the MIN_ENGINE check (and the
engine download) only ran when setup was run again with its options.  Now every start checks the installed
engine and replaces it when it is older than MIN_ENGINE.  If it cannot (no internet, the model still running,
no ready-made engine for this GPU) the installed engine is kept and starts as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
- GPU experts: IQ4_XS gate/up (the IQ3_S file keeps layer 47's routed experts in it), transcribed from llama.cpp's
  vec_dot_iq4_xs_q8_1; checked on all 48 layers against ggml's float reference (native_expert_parity: 0 failures).
- MTP draft head: gather_rows takes rows of any size (the IQ3_S head is Q6_K, 2100-byte rows, not a multiple of 16).
- setup: IQ3_S for the original model (Swift 1.5 has none), 64 GB RAM, context capped at 128K like IQ3_XXS.
- README / DETAILS: IQ3_S speed (RTX 5070, Ryzen 5 7600, 64 GB): 54 tok/s at 4K, 42 at 128K, 374-397 prompt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
A missing, null, 0 or -1 max_tokens / max_completion_tokens now means the
rest of the context (minus the 8-token margin strata --serve keeps), so
replies no longer stop at 1024 tokens. prepare() resolves the budget after
tokenizing and returns it; an explicit budget is honoured exactly and still
rejected when prompt + budget exceeds the context. Adds serve/test_server.py.
Temperature was ignored because the verify window's head sampling was
pinned to greedy INSIDE the captured CUDA graph (verify.cpp hardcoded
sp.greedy = true), and the GEN protocol carried no sampling keys.

- the sampling moves OUT of the captured graph: the graph computes the
  head logits, and run() applies sample_tokens host-side after the
  replay, with that request's own parameters and a fresh Philox counter
  per window (a captured sampler would replay the same draws forever)
- the GEN protocol accepts optional key=value pairs before the ids:
  temperature=, top_p=, top_k=, seed=; absent keys stay greedy, exactly
  as before
- server.py forwards the request's temperature/top_p/top_k/seed
- emitted tokens are always the TARGET's own samples (the drafts only
  fill the verified window), so the output distribution is the target's
  regardless of what the drafts propose

Building on the conversation cache: greedy requests stay bit-identical,
sampled requests with the same seed replay identically.
…lt top_k=20

The sampled path requires top_k in 1..64 (the selection loop is
O(k*n_vocab) by design); with the engine-default top_k=0 it returned
without writing the token, leaking stale buffer contents as output.
Sampled requests now default to the sampler's own top_k=20.

The Philox draw counter also must persist across a request's verify
windows - it was read from a run-local variable that reset every
window, so every window redrew the same numbers (identical sampled
output on identical requests).
The sampled chain ran in ONE thread per token (64-thread launch): top_k
alone was k sequential full-vocabulary scans with an inner sweep over the
taken list, and a verify window measured ~1.6 s in the sampler - sampled
requests decoded at 1.5 tok/s against 54 greedy.

The selection is k argmax rounds, so it parallelises exactly like the
greedy kernel: one block per token, the rounds back to back inside it,
each a two-level (warp, then block) reduction.  Ties resolve to the
lowest index (the serial scan's strict '>'), so the kept sequence - set
and order - is unchanged, and the top_p cut, temperature and Philox draw
read that order as before.  sampler_parity passes; a 248,320-vocab
GPU-vs-CPU draw check matches the serial reference on five parameter
sets including injected duplicate logits.

Measured (RTX 4070 TS, IQ3_XXS, 128-token generations): sampled decode
1.50 -> 42.7 tok/s; greedy decode unchanged (53.3 vs 54.3 tok/s median,
within run noise; greedy launches the untouched kernel).
StrataEngine.generate accepted the request's sampling parameters and
dropped them, so every served request decoded greedy whatever the client
asked - the README's 'temperature / top_p / top_k / seed are honored per
request' was not true over the HTTP API, and the engine-side per-request
sampling was unreachable from it.

The keys ride the GEN line in the engine's own spelling; an absent
temperature keeps the engine's greedy default and temperature=0 is not
forwarded (it means the same thing); top_k outside the sampled path's
1..64 is dropped rather than sending a value the engine would refuse.

Verified end to end over the HTTP server (static residency for the
seeded leg): temperature=0 and absent agree token for token; a
temperature=0.8 request differs from greedy; two temperature=0.8
seed=99 requests return identical text.
It contradicted the paragraph next to it, and since the sampling keys
now ride the GEN line (and the block-parallel sampler makes a sampled
window cost about what a greedy one does), it was simply false.
The run config's optional `sampling` block (temperature, top_p, top_k,
seed) sets defaults for the fields a request leaves out, so a client
that sends no sampling keys gets configured sampling instead of greedy.
The request's own fields always win: an explicit temperature=0 still
means greedy, and a field set to null falls back to the default.

Keys the sampled path does not implement yet (min_p, presence_penalty,
repetition_penalty, frequency_penalty) are named at startup and ignored
rather than silently dropped; unknown keys are reported the same way.
An invalid value (top_k outside 1..64, a non-positive seed, a negative
temperature) refuses to start the server - a typo'd config should not
quietly change sampling.

Verified with a spy engine through Service.run: defaults apply when the
request omits the fields, the request's own values win, a null field
falls back to the default, and a server without the block forwards the
request unchanged; the config validator rejects out-of-range values and
warns on the not-yet-implemented keys.
The sampled chain's last two gaps close: `min_p` and the llama.cpp
penalty set (repeat/freq/present over a history window) are live end
to end - GEN keys, per-request params, and the token history the
penalties count over.

- GEN accepts min_p=F penalty_last_n=N penalty_repeat=F penalty_freq=F
  penalty_present=F between max_new and the ids (the server's
  spelling); the engine clamps defensively (min_p to 0..1, a negative
  last_n to 0) and skips what it does not know, as before.
- min_p cuts the top_k survivors to the prefix with p >= min_p * p_max
  - in logits, sel_logit >= sel_logit[0] + logf(min_p) - before top_p,
  so the two cuts compose; 0 disables, and the head always survives,
  so the kept count never reaches zero.
- the penalties need the token history: the serve loop uploads the
  last penalty_last_n tokens - the consumed tail plus the fed-back
  head, which joins consumed_seq only after the window commits - into
  one device row before every window.  A request without penalties
  hands the sampler a null row and takes the byte-for-byte neutral
  path (no upload, nothing handed over).

Verified: sampler_parity grows a full-chain host reference (the Philox
draw transcribed host-side) and fixtures for the sampled chain with
penalties and for min_p at 0/0.5/0.9 - the kernel matches the
reference on all of them, and each fixture first asserts it can SEE
the feature before asserting the kernel matches.  On the IQ3_XXS
artifact, same seed: a request with neutral penalty keys is
token-identical to one without them; a six-copy prompt that loops
24/24 on one token clean produces 24 distinct tokens with repeat 1.5
+ present 0.5; min_p 0.9 changes the pick.
min_p, presence_penalty, repetition_penalty and frequency_penalty move
from "warned and ignored" to the sampled path, and penalty_last_n sets
the window they count over - the server sends the engine's default of
64 when a penalty is set and the client left the window out.
sampling_keys forwards each field only when it is not neutral, so a
client (or a config) that leaves them out costs nothing.  The
validator range-checks the new fields and still refuses to start the
server on a value it cannot honour.

Verified: the validator accepts and range-checks min_p/presence/
frequency/repetition/penalty_last_n; the GEN line spells them the
engine's way and omits neutral values; the last-64 default appears
only when a penalty is actually set.
The penalties made the sampled path pay O(k x n_vocab x hlen) integer
compares per token: every top_k argmax round recounted every
candidate's history occurrences (k=20 rounds, hlen=64, ~318M compares
on the 248,320 vocabulary) - measured 45.2 -> 30.8 tok/s on a real
chat workload.  But the history touches at most hlen DISTINCT tokens,
so membership is a bit: each block now builds a shared bitmap from its
history row (atomicOr, one pass) and a candidate pays the O(hlen)
count scan only when its bit is set - at most hlen x k scans per token
instead of k x n_vocab.  Neutral values and the no-history path skip
the bitmap entirely.

The counts - and therefore every penalised value, kept token and
draw - are exactly what the per-candidate scan produced;
sampler_parity passes unchanged, plus a new fixture pinning the
window clamp (an 8-entry history at penalty_last_n=4 must punish only
the tail half).
From 64K context the KV cache lives in pinned RAM and a VRAM block cache holds the part the sparse attention reads
(--kv-resident N; setup turns it on with 32768 when RAM allows). Blocks the indexer selects are made resident on
the device inside the captured graphs (clock eviction, zero-copy reads of misses); writers write through, so the
attention reads exactly the same values. The freed VRAM goes to the expert cache: Q2_0 at 262K decodes 62.6 tok/s
instead of 50.9. The MTP drafter keeps its window in a ring. New ctest kv_stream_parity. Also wires the opt-in
suffix drafter (--suffix-draft) into the engine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
… is left (engine 0.1.6)

The MTP drafter's ring over a host copy is set up after the expert arena has pinned what it could; when that
pinned allocation fails the drafter now falls back to its fully resident KV instead of stopping the engine
("mtp: state init failed", seen with Swift 1.5 at 262K). setup requires engine 0.1.6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…ne 0.1.7)

- Prompt lookup on by default: the suffix drafter's windows are taken only where a learned policy (measured
  acceptance per match length, measured window cost) expects more tokens per ms than the MTP window; lookup windows
  up to --spec + 2. Code edits +6-11%, other text unchanged, verified output identical. --suffix-draft 0 = off.
- Per-request sampling (Niko1221#19) keeps greedy requests on the in-graph sampler (no extra launch per window), and draws
  row t of a window with Philox(seed, pos0 + t), so a seed replays the same text whatever the drafts were.
- tools: a shallow install (G:\Strata) no longer crashes the MTP packer and the pack tools (IndexError, Niko1221#20).
- setup: says at step 1 when the PC has less RAM than the smallest model needs; requires engine 0.1.7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
- Implement Q4_0 KV quantization and dequantization kernels with scale/offset
- Implement fast Walsh-Hadamard transform (FWHT-256) rotation for outlier suppression
- Add Q4_0 attention decode support in qsa_decode_attn
- Add kv_q4_parity parity test verifying CPU vs CUDA numerical accuracy
- Support --kv q4_0 in generate CLI and runtime layer allocation
- Update .gitignore for build and repository hygiene
- Implement Q4_0 KV quantization and dequantization kernels with scale/offset
- Implement fast Walsh-Hadamard transform (FWHT-256) rotation for outlier suppression
- Add Q4_0 attention decode support in qsa_decode_attn
- Add kv_q4_parity parity test verifying CPU vs CUDA numerical accuracy
- Support --kv q4_0 in generate CLI and runtime layer allocation
- Update .gitignore for build and repository hygiene
…egrated with KV streaming; accurate expert-cache note (Niko1221#23)

PR Niko1221#21 (code-martin): a 4-bit K/V format (orthonormal FWHT-256 rotation of K, V and Q, then q4_0 blocks). On merge:
- Q4 is a full third format of the KV-streaming code (host copy, block residency, prompt-path staging, MTP ring,
  STATE_HASH): with --kv-resident the PR wrote K/V through the residency map's -1 entries. kv_stream_parity now
  checks Q4 streamed vs resident, bitwise.
- The per-token path ran attention twice (once before the blocks were resident); once now.
- The q4 group maximum breaks |x| ties the same way in every lane (one scale per block).
- Dropped: the duplicate path fixes (in 0.1.7), personal .gitignore entries, the re-encoded CMakeLists.
- Measured (bench/results/2026-09-27-kv-q4): int8 matches fp16; q4_0 costs precision on long documents
  (perplexity +8-12%), needles 5/5, +3.7% decode at 128K. Opt-in: setup asks above 8K context (int8 recommended),
  START-HERE.bat --kv q4_0, or --kv q4_0 in the config.

Niko1221#23: the start-up warning "the GPU hit path is NOT CORRECT" dated from a long-fixed fault. Measured teacher-forced
on 2,557 tokens (bench/results/2026-09-27-cache-parity): cache on vs off, 95-98% same top-1 and equal perplexity
(-0.005 +- 0.005 nats); the warning is now one accurate line.

Co-Authored-By: code-martin <code-martin@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
The browser page at http://127.0.0.1:8080 is now the Strata app, in the
Strata design language (serve/web, same origin, no CDN):
- Chat: streaming answers, collapsible thinking, Markdown with code copy,
  images when enabled, sampling/thinking settings, export, new chat.
- Monitor: live model state (reading with progress / generating at tok/s),
  GPU load, VRAM, temperature, power, PCIe traffic, CPU, RAM, disk, the
  context fill and the last requests, each with a minute of history.
- About: the model and engine settings, this PC, the API addresses.

Server: GET /metrics (API key when set), /web/* app files, /fonts/* only
when installed locally (not shipped; the page falls back to the system
font). serve/telemetry.py reads NVML via ctypes and psutil (new setup
dependency) with fallbacks. The engine prints an INFO line before READY
(kv, expert slots, speculation); older servers ignore it and the server
works with older engines.

Based on the dashboard idea in PR Niko1221#22 by code-martin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…il picked; engine 0.1.8

- serve/web/fonts: Outfit as one variable font (latin + latin-ext, weights
  100-900) with its OFL.txt, in place of the design kit's commercial font.
- The page no longer saves the system's theme on first load (which froze
  it); only a click on the theme button or toggle is remembered, and the
  page follows system changes until then.
- setup: requires engine 0.1.8 (--kv q4_0, the INFO line).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
Niko1221 and others added 20 commits September 29, 2026 19:59
…pt pass in batches); setup requires it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…experts.bin; setup uses it when the model's experts do not fit the RAM beside the OS (a 32 GB PC with a big GPU: the experts come from the file cache, the GPU holds the most-used)
…ment

Measured engine knobs (STRATA_HIPBLASLT_TUNING, STRATA_PF_EMBED_BATCH,
STRATA_PREFILL_RING) previously lived only in one systemd unit's command line
and vanished on any restart. The config's 'env' now wins over the inherited
environment, so a promoted A/B state is expressed entirely in the config file.

Verified on the gfx1100 rollout: the promoted resident serves with
'prefill gemm: hipBLASLt tuning enabled (30 rows, gfx1100, version 100100)'
read from the config-driven env; paired A/B (results/tune-20260929T175811Z,
T184449Z): long-prompt prefill 416->971 tok/s, decode -0.8% (inside the 3%
protected cap), winner-vs-original decode +38.3%.
…K than 0.1.22)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
The tensor-core prompt-attention kernel is already refused at runtime on AMD
(line 695), but the file unconditionally included <math_constants.h> for
CUDART_INF_F, which is not present on a ROCm-only build. Replace it with the
portable INFINITY macro from <cmath> so the HIP build succeeds without a CUDA
 toolkit installed.
Rebased onto main (0.1.22): the portable pre-sm_80 QSA scorer and the sm_80
guard on the tensor-core prompt attention are already in main, so this is
the rest of the port.

- the runtime floor: device_info refuses anything older than compute
  capability 7 (main's runtime check still requires sm_120, so a binary
  built for sm_75 would never start),
- fused_gr: the down kernel carries the tokens in slices that fit the
  card's shared-memory opt-in (Turing, 64 KB: 6-token slices; 4 tokens when
  the card reports no opt-in, under the 48 KB default), merged on top of
  main's per-device opt-in - full 8-token windows no longer fail on Turing
  with "invalid argument",
- the setup/CMake floor is 7.5 (RTX 20 / 30 / 40 / 50) without an
  experimental flag: CMake's arch refusals, setup.py's gpu_problem and the
  GPU hints and error texts,
- docs/MULTI_GPU.md says what a Turing card actually runs.

Verified end-to-end on RTX 2070 (sm_75, 0.1.20 base): model loads, OpenAI/
Anthropic API serving works, greedy output identical to the sm_120 build.

Merged (PR Niko1221#87) with two changes: the runtime floor is 7.5 exactly (major*10+minor < 75; the PR let Volta 7.0/7.2
through), kept beside main's HIP check; and a card reporting no shared-memory opt-in slices the tokens to what fits the
48 KB default for its own tile (4 tokens on CUDA, all 8 with HIP's smaller tile, so gfx1100 keeps one launch).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…me text in a message stays text (Niko1221#150)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…the engine is compiled again for a card it has no code for, at setup and at start (Niko1221#128)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…ts added (Niko1221#137); setup replaces an older shipped subset at setup and at start, a hand-made one is kept

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
… compiled here when it has no code for the card (BUILD.json vision_archs)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
… Korean tokens (40,525 -> 106,299 ids; Niko1221#137). Q2_0 on CJK answers +15-38% decode, English -1-2%, the same expert slots on 12 GB

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…anese and Korean), RTX 20 cards, the HIP build without CUDA headers; setup requires it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
# Conflicts:
#	CMakeLists.txt
#	README.md
#	docs/AMD_HIP.md
#	docs/AMD_HIP_PERFORMANCE.md
#	include/strata/hip_compat/cuda_runtime.h
#	src/core/device.cu
#	src/kernels/cuda/qsa_prompt_attn.cu
#	src/kernels/cuda/qsa_select.cu
#	src/prefill/prefill.cpp
#	tests/hip/prefill_native_batch.cpp
#	tests/platform/direct_file_async.cpp
Re-application of d7a0d6b after the 0.1.27 merge (the upstream tree still has
both defects): qsa_prompt_attn_parity links CUDA::cudart unconditionally, and
native_mmvq_multi names bench/micro/native_mmvq_multi.cpp, a source in no commit
of this repository or upstream. Verified by a HIP-only configure with
STRATA_BUILD_TESTS=ON and the CTest run.
telemetry.py reads the GPU through NVML, so on an AMD box (no libnvidia-ml)
every GPU series is absent: the Monitor's GPU load, VRAM, temperature, power and
PCIe cards show "-", and hardware_static.gpu_name is null.  Where psutil is not
installed, the disk rate shows "needs psutil (setup installs it)" and the
physical-core count is missing.

_AmdSysfs reads the same fields from amdgpu's sysfs: the card is found by
scanning /sys/class/drm/card*/device for DRIVER=amdgpu (the number is not fixed
- a box with another DRM device can have the AMD card at card1), the
temperature is the hwmon sensor labelled "edge" (what NVML calls the GPU
temperature), power comes from power1_average/power1_cap, and the link from
current/max_link_speed plus current_link_width.  Every reading is a small file
read, so a sample cannot block the server, and anything the kernel does not
expose stays None rather than being invented.  amdgpu has no PCIe throughput
counter, so the link is reported and the MB/s series stays empty; the web app
already renders "Gen4 x16" from those fields.

The patch also adds /proc/diskstats (the source psutil itself reads; whole disks
only, never partitions and never loop/zram/dm, which would count a device-mapper
layer twice) and /proc/cpuinfo's (physical id, core id) pairs for the
psutil-less case.

NVML still wins whenever it loads, and the psutil path is untouched, so an
NVIDIA install behaves exactly as before.  Tests: serve/test_telemetry.py needs
no GPU - the pure helpers, the no-card case, and a sample that must still carry
every series the Monitor plots.

(Rebased onto the multi-card Monitor f7262d0: the AMD backend now feeds the
per-card readings and their total/mean/hottest aggregation of issue Niko1221#112 too.
NVML still wins when it loads - the follow-up commit changes that for AMD.)
Two rules for boxes where the amdgpu scan or NVML alone picked the wrong card:

- The _AmdSysfs card is the one at the model device's PCI slot (HIP ordinal ->
  PCI slot via libamdhip64, matched to the DRM card's uevent), not 'the index-th
  amdgpu' - a Ryzen iGPU is amdgpu too and takes index 0 of the card scan.
- When the model runs on the AMD card the readings come from amdgpu's sysfs even
  where NVML loads (it ships with the NVIDIA driver and used to win), because
  0.1.26's setup lets a mixed PC choose AMD.  'Runs on AMD' is the config's
  backend key (setup's own signal) or, without one, the engine binary's own
  linkage to libamdhip64 (a string-table read; the binary is not executed).

On NVIDIA-only boxes nothing changes: NVML still wins there.

Tests: serve/test_telemetry.py 8/8 with a fake sysfs (iGPU vs dGPU selection),
fake backends (AMD wins over a loading NVML exactly when the model is on AMD)
and the backend decision cases.  docs/AMD_HIP.md no longer says the Monitor has
no GPU statistics.
The fork's clock64() stamp variant conflicted with upstream's wall_clock64()
rework at the merge (the splice did not compile).  The wall-clock form is kept:
it reports true nanoseconds, which is what the verify-window profiler's span
labels assume; the clock64 form's 'relative spans only' caveat is a downgrade.
This is the exact tree the 0.1.27 gates and the version A/B ran on.
@Niko1221

Niko1221 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Heads-up: today we cleaned up this repository's commit history, and GitHub closed this PR automatically when main was force-pushed (that is the "base branch was force-pushed" event above). It is not a rejection.

GitHub cannot reopen it, because its branch is built on the old history. If you want to keep it going: start a new branch from the new main, cherry-pick your own commits onto it (git fetch upstream && git switch -c my-fix upstream/main && git cherry-pick <your commits>), and open a new PR. Or simply leave it: when we take your change for a release we port it onto the new main ourselves and keep you as its author.

Sorry for the noise, and thank you for the contribution.

@xyzzing

xyzzing commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Re-filed as #1297 — shipping the ported + measured version (not the 0.1.27-era patch this branch carried); re-based onto the new main with calibrate.py reconciled against its new host_worker_extras machinery; test ladder green on this base.

@Niko1221

Niko1221 commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Thank you, xyzzing. The decode autotune found the shipped defaults already win (deltas under 1%), and the release workflow with contents: write is not something we can take, so this stays closed. The one useful result, --spec-min-p 0.70 at +10%, is a setting; a small PR or issue with that A/B is welcome. The PR was closed by the history rewrite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.