From 002e3d7d13c414ea874abbee8d82803dd3a72fe5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Fri, 2 Oct 2026 10:58:21 -0400 Subject: debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop --- tools/rmtop/src/main.c | 323 ++++++++++++++++++++++++++++++++++++++++++++++++ tools/rmtop/src/panel.c | 232 ++++++++++++++++++++++++++++++++++ 2 files changed, 555 insertions(+) create mode 100644 tools/rmtop/src/main.c create mode 100644 tools/rmtop/src/panel.c (limited to 'tools/rmtop/src') diff --git a/tools/rmtop/src/main.c b/tools/rmtop/src/main.c new file mode 100644 index 0000000..b7985df --- /dev/null +++ b/tools/rmtop/src/main.c @@ -0,0 +1,323 @@ +/* main.c - rmtop: ncurses real-time read-only GPU dashboard. + * + * Interactive mode needs a terminal; --once renders the same panels as plain + * table/json/csv text for scripting and harness capture. All sampling is + * read-only (RM control queries and /proc/sysfs reads); no write path exists. + */ +#include +#include +#include +#include +#include +#include +#include + +#include "rmtop.h" + +static void usage(void) +{ + printf("rmtop %s - read-only NVIDIA GPU dashboard (Open-IOV)\n\n", + RMTOP_VERSION); + printf("usage: rmtop [-g N|BDF] [--interval=MS] [--once] " + "[--format=...]\n\n" + " -g N|BDF select GPU (default: first)\n" + " --interval=MS refresh interval in ms (default 500)\n" + " --once render one snapshot as plain text and exit\n" + " --format=FMT table|json|csv (with --once)\n" + " --help, --version\n\n" + "keys: q quit r refresh s interval p pause h help\n" + " g/G next/previous GPU\n\n" + "Live engine utilization, power and thermals are marked n/a in\n" + "this read-only minimal build; see docs/17-RM-HARNESS.md.\n"); +} + +static int parse_common(struct rm_ctx *ctx, int *interval_ms, int *once, + int argc, char **argv) +{ + static const struct option long_opts[] = { + { "device-spec", required_argument, NULL, 'g' }, + { "interval", required_argument, NULL, 'i' }, + { "once", no_argument, NULL, 'o' }, + { "format", required_argument, NULL, 'f' }, + { "device", required_argument, NULL, 1000 }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 1001 }, + { NULL, 0, NULL, 0 }, + }; + int opt; + + optind = 1; + opterr = 0; + while ((opt = getopt_long(argc, argv, "g:i:of:h", long_opts, + NULL)) != -1) { + if (opt == 'g' || opt == 'f') { + if (rm_apply_common_opt(ctx, opt == 'g' ? 'd' : 'f', + optarg) < 0) + return -1; + continue; + } + if (opt == 'i') { + *interval_ms = atoi(optarg); + if (*interval_ms < 100) + *interval_ms = 100; + continue; + } + if (opt == 'o') { + *once = 1; + continue; + } + if (opt == 'h') { + usage(); + exit(0); + } + if (opt == 1000) { + ctx->dev_path = optarg; + continue; + } + if (opt == 1001) { + printf("rmtop %s (read-only; Open-IOV)\n", + RMTOP_VERSION); + exit(0); + } + return -1; + } + return 0; +} + +static void draw_bar(WINDOW *win, int y, int x, int width, double ratio, + const char *label) +{ + int filled = (int)(ratio * width); + int i; + + if (filled < 0) + filled = 0; + if (filled > width) + filled = width; + mvwprintw(win, y, x, "%-10s [", label); + for (i = 0; i < width; i++) { + if (i < filled) + waddch(win, ACS_CKBOARD); + else + waddch(win, '-'); + } + wprintw(win, "] %5.1f%%", ratio * 100.0); +} + +void rmtop_render_curses(const struct rmtop_state *st, int interval_ms, + int paused) +{ + char when[32]; + struct tm tm; + struct timespec ts; + int row = 1; + + clock_gettime(CLOCK_REALTIME, &ts); + localtime_r(&ts.tv_sec, &tm); + strftime(when, sizeof(when), "%H:%M:%S", &tm); + + erase(); + attron(A_REVERSE); + mvprintw(0, 0, " rmtop %s | GPU %d: %.40s | %.50s | %s%s ", + RMTOP_VERSION, st->gpu_minor, + st->model[0] ? st->model : "-", + st->driver[0] ? st->driver : "driver unknown", when, + paused ? " | PAUSED" : ""); + clrtoeol(); + attroff(A_REVERSE); + + mvprintw(row++, 1, "Bus %-14s UUID %-40s", + st->bdf[0] ? st->bdf : "-", + st->uuid[0] ? st->uuid : "-"); + mvprintw(row++, 1, "RM client: %s%s", + st->have_rm ? "ok" : "unavailable", + st->have_rm && st->rm_name[0] ? " " : ""); + if (st->have_rm && st->rm_name[0]) + printw("%s", st->rm_name); + mvprintw(row++, 1, "Arch: %s (0x%x/0x%x rev %u)", + st->have_rm ? rm_arch_str(st->mc_arch) : "-", + st->mc_arch, st->mc_impl, st->mc_rev); + mvprintw(row++, 1, "VRAM: %u MiB total, %u MiB free, %u MiB used", + st->vram_total_kib / 1024, st->heap_free_kib / 1024, + (st->vram_total_kib - st->heap_free_kib) / 1024); + mvprintw(row++, 1, "BAR1: %u MiB size, %u MiB available", + st->bar1_size_kib / 1024, st->bar1_avail_kib / 1024); + row++; + mvprintw(row++, 1, "Engines (%u)%s:", st->engine_count, + st->engine_names_ok ? "" : " [FIFO names unavailable]"); + if (st->engine_count > 0) { + unsigned int i; + + move(row, 3); + for (i = 0; i < st->engine_count; i++) { + const char *name = st->engine_names_ok ? + st->engine_names[i] : "-"; + + printw("%s ", name); + if ((i + 1) % 8 == 0 && i + 1 < st->engine_count) + mvprintw(++row, 3, " "); + } + row++; + } + row++; + if (st->have_timer) { + time_t secs = (time_t)(st->timer_nsec / 1000000000ULL); + + gmtime_r(&secs, &tm); + strftime(when, sizeof(when), "%Y-%m-%dT%H:%M:%S", &tm); + mvprintw(row++, 1, "GPU timer: %llu ns (%sZ)", + (unsigned long long)st->timer_nsec, when); + } + row++; + { + double total = st->vram_total_kib ? st->vram_total_kib : 1.0; + + draw_bar(stdscr, row++, 1, 40, + (double)(st->vram_total_kib - st->heap_free_kib) / + total, "VRAM"); + } + draw_bar(stdscr, row++, 1, 40, + st->bar1_size_kib ? + (double)(st->bar1_size_kib - st->bar1_avail_kib) / + st->bar1_size_kib : 0.0, "BAR1"); + row++; + attron(A_DIM); + mvprintw(row++, 1, "Live util/power/thermal: n/a in this read-only " + "minimal build (see docs/17-RM-HARNESS.md)"); + attroff(A_DIM); + + attron(A_REVERSE); + mvprintw(LINES - 1, 0, " q quit | r refresh | s %dms interval | " + "p pause | g/G GPU | h help ", interval_ms); + clrtoeol(); + attroff(A_REVERSE); + refresh(); +} + +static void help_screen(void) +{ + erase(); + mvprintw(0, 1, "rmtop help"); + mvprintw(2, 1, "This dashboard samples read-only RM controls and " + "/proc/sysfs."); + mvprintw(3, 1, "It never allocates, maps, resets or claims the GPU."); + mvprintw(5, 1, "q / F10 quit"); + mvprintw(6, 1, "r refresh now"); + mvprintw(7, 1, "s cycle sample interval (250/500/1000/2000 ms)"); + mvprintw(8, 1, "p pause/resume sampling"); + mvprintw(9, 1, "g / G next / previous GPU"); + mvprintw(10, 1, "h this help"); + mvprintw(12, 1, "Press any key to return."); + refresh(); + timeout(-1); + getch(); + timeout(0); +} + +int main(int argc, char **argv) +{ + struct rm_ctx ctx; + struct rmtop_state st; + int interval_ms = 500; + int once = 0; + int paused = 0; + int rc; + + ctx.gpu_minor = -1; + ctx.dev_path = NULL; + ctx.format = RM_FMT_TABLE; + ctx.have_rm = 0; + ctx.rm.fd = -1; + + if (parse_common(&ctx, &interval_ms, &once, argc, argv) < 0) { + usage(); + return 2; + } + + if (once) { + rmtop_collect(&ctx, &st); + rmtop_render_plain(&st, ctx.format); + if (ctx.have_rm) + rm_client_close(&ctx.rm); + return 0; + } + + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + curs_set(0); + if (has_colors()) { + start_color(); + use_default_colors(); + init_pair(1, COLOR_GREEN, -1); + init_pair(2, COLOR_YELLOW, -1); + init_pair(3, COLOR_RED, -1); + } + + rc = 0; + while (1) { + int ch; + + if (!paused) { + rmtop_collect(&ctx, &st); + rmtop_render_curses(&st, interval_ms, paused); + timeout(interval_ms); + } else { + timeout(-1); + } + ch = getch(); + switch (ch) { + case 'q': + case 'Q': + case KEY_F(10): + goto done; + case 'r': + break; + case 's': + if (interval_ms == 250) + interval_ms = 500; + else if (interval_ms == 500) + interval_ms = 1000; + else if (interval_ms == 1000) + interval_ms = 2000; + else + interval_ms = 250; + break; + case 'p': + paused = !paused; + break; + case 'g': + case 'G': { + struct rm_gpu_desc gpus[RM_MAX_GPUS]; + int n = rm_scan_gpus(gpus, RM_MAX_GPUS); + int i; + + for (i = 0; i < n; i++) { + if (gpus[i].minor == ctx.gpu_minor) { + int next = (ch == 'g') ? + (i + 1) % n : (i + n - 1) % n; + + rm_client_close(&ctx.rm); + ctx.have_rm = 0; + ctx.gpu_minor = gpus[next].minor; + break; + } + } + break; + } + case 'h': + help_screen(); + break; + case KEY_RESIZE: + break; + default: + break; + } + } +done: + endwin(); + if (ctx.have_rm) + rm_client_close(&ctx.rm); + return rc; +} diff --git a/tools/rmtop/src/panel.c b/tools/rmtop/src/panel.c new file mode 100644 index 0000000..9ad5ac8 --- /dev/null +++ b/tools/rmtop/src/panel.c @@ -0,0 +1,232 @@ +/* panel.c - rmtop sampling (read-only RM + /proc) and plain rendering. */ +#include +#include +#include + +#include "rmtop.h" + +static int pick_gpu(struct rm_ctx *ctx, struct rmtop_state *st) +{ + struct rm_gpu_desc gpus[RM_MAX_GPUS]; + const struct rm_gpu_desc *g = NULL; + int n, i; + + n = rm_scan_gpus(gpus, RM_MAX_GPUS); + if (n <= 0) { + st->gpu_minor = ctx->gpu_minor; + return n; + } + for (i = 0; i < n; i++) { + if (gpus[i].minor == ctx->gpu_minor) + g = &gpus[i]; + } + if (g == NULL) + g = &gpus[0]; + st->gpu_minor = g->minor; + snprintf(st->model, sizeof(st->model), "%s", g->model); + snprintf(st->bdf, sizeof(st->bdf), "%s", g->bdf); + snprintf(st->uuid, sizeof(st->uuid), "%s", g->uuid); + return n; +} + +static void read_driver_version(struct rmtop_state *st) +{ + FILE *fh = fopen("/proc/driver/nvidia/version", "r"); + + st->driver[0] = '\0'; + if (fh == NULL) + return; + if (fgets(st->driver, sizeof(st->driver), fh) != NULL) + st->driver[strcspn(st->driver, "\r\n")] = '\0'; + fclose(fh); +} + +int rmtop_collect(struct rm_ctx *ctx, struct rmtop_state *st) +{ + NV2080_CTRL_MC_ARCH_INFO_PARAMS arch; + NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS engines; + NV2080_CTRL_TIMER_GET_TIME_PARAMS timer; + NV2080_FIFO_DEVICE_ENTRY *fifo; + NvU32 fb_indices[6] = { + NV2080_CTRL_FB_INFO_INDEX_TOTAL_RAM_SIZE, + NV2080_CTRL_FB_INFO_INDEX_USABLE_RAM_SIZE, + NV2080_CTRL_FB_INFO_INDEX_HEAP_SIZE, + NV2080_CTRL_FB_INFO_INDEX_HEAP_FREE, + NV2080_CTRL_FB_INFO_INDEX_BAR1_SIZE, + NV2080_CTRL_FB_INFO_INDEX_BAR1_AVAIL_SIZE, + }; + NvU32 fb_values[6] = { 0 }; + NvU32 n_fifo = 0; + unsigned int i; + + memset(st, 0, sizeof(*st)); + st->gpu_minor = ctx->gpu_minor; + st->sampled_at = time(NULL); + read_driver_version(st); + pick_gpu(ctx, st); + + st->rm_rc = rm_ctx_open(ctx); + if (st->rm_rc != 0) + return 0; + st->have_rm = 1; + st->gpu_minor = ctx->rm.gpu_minor; + + if (rm_query_name(ctx, st->rm_name, sizeof(st->rm_name)) != 0) + snprintf(st->rm_name, sizeof(st->rm_name), "%s", "-"); + + memset(&arch, 0, sizeof(arch)); + if (rm_query_mc_arch(ctx, &arch) == 0) { + st->mc_arch = arch.architecture; + st->mc_impl = arch.implementation; + st->mc_rev = arch.revision; + st->mc_subrev = arch.subRevision; + } + + if (rm_query_fb(ctx, fb_indices, fb_values, 6) == 0) { + st->vram_total_kib = fb_values[0]; + st->vram_usable_kib = fb_values[1]; + st->heap_size_kib = fb_values[2]; + st->heap_free_kib = fb_values[3]; + st->bar1_size_kib = fb_values[4]; + st->bar1_avail_kib = fb_values[5]; + } + + memset(&timer, 0, sizeof(timer)); + { + int ioctl_ret; + + if (rm_control(&ctx->rm, ctx->rm.hSubdevice, + NV2080_CTRL_CMD_TIMER_GET_TIME, &timer, + sizeof(timer), &ioctl_ret) == 0 && + ioctl_ret == 0) { + st->timer_nsec = timer.time_nsec; + st->have_timer = 1; + } + } + + memset(&engines, 0, sizeof(engines)); + if (rm_query_engines(ctx, &engines) == 0) { + st->engine_count = engines.engineCount; + if (st->engine_count > RMTOP_MAX_ENGINES) + st->engine_count = RMTOP_MAX_ENGINES; + for (i = 0; i < st->engine_count; i++) + st->engine_list[i] = engines.engineList[i]; + } + fifo = calloc(1, sizeof(*fifo) * 512); + if (fifo != NULL) { + rm_query_fifo_table(ctx, fifo, 512, &n_fifo); + for (i = 0; i < st->engine_count; i++) { + snprintf(st->engine_names[i], + sizeof(st->engine_names[i]), "%s", + rm_engine_name(fifo, n_fifo, + st->engine_list[i])); + } + st->engine_names_ok = st->engine_count > 0; + free(fifo); + } + return 0; +} + +void rmtop_render_plain(const struct rmtop_state *st, enum rm_format format) +{ + struct rm_kv kv[16]; + char v[16][160]; + char human[32], human2[32]; + char when[32]; + struct tm tm; + int n = 0; + + if (format != RM_FMT_TABLE) + goto build; + printf("rmtop %s - read-only GPU dashboard (one-shot)\n", + RMTOP_VERSION); + printf("sampled %s", ctime(&st->sampled_at)); +build: + kv[n].key = "gpu_minor"; + snprintf(v[n], sizeof(v[n]), "%d", st->gpu_minor); + kv[n].value = v[n]; + n++; + kv[n].key = "model"; + kv[n].value = st->model[0] ? st->model : "-"; + n++; + kv[n].key = "driver"; + kv[n].value = st->driver[0] ? st->driver : "-"; + n++; + kv[n].key = "bus"; + kv[n].value = st->bdf[0] ? st->bdf : "-"; + n++; + kv[n].key = "uuid"; + kv[n].value = st->uuid[0] ? st->uuid : "-"; + n++; + kv[n].key = "rm_name"; + kv[n].value = st->have_rm && st->rm_name[0] ? st->rm_name : "-"; + n++; + kv[n].key = "rm_client"; + snprintf(v[n], sizeof(v[n]), "%s", + st->have_rm ? "ok" : "unavailable"); + kv[n].value = v[n]; + n++; + if (st->have_rm) { + snprintf(v[n], sizeof(v[n]), "%s (0x%x/0x%x rev %u)", + rm_arch_str(st->mc_arch), st->mc_arch, + st->mc_impl, st->mc_rev); + kv[n].key = "arch"; + kv[n].value = v[n]; + n++; + } + rm_human_kib(st->vram_total_kib, human, sizeof(human)); + rm_human_kib(st->vram_usable_kib, human2, sizeof(human2)); + snprintf(v[n], sizeof(v[n]), "%u KiB (%s) / usable %u KiB (%s)", + st->vram_total_kib, human, st->vram_usable_kib, human2); + kv[n].key = "vram"; + kv[n].value = v[n]; + n++; + rm_human_kib(st->heap_free_kib, human, sizeof(human)); + rm_human_kib(st->heap_size_kib, human2, sizeof(human2)); + snprintf(v[n], sizeof(v[n]), "free %u KiB (%s) / heap %u KiB (%s)", + st->heap_free_kib, human, st->heap_size_kib, human2); + kv[n].key = "vram_free"; + kv[n].value = v[n]; + n++; + rm_human_kib(st->bar1_size_kib, human, sizeof(human)); + rm_human_kib(st->bar1_avail_kib, human2, sizeof(human2)); + snprintf(v[n], sizeof(v[n]), "size %u KiB (%s), avail %u KiB (%s)", + st->bar1_size_kib, human, st->bar1_avail_kib, human2); + kv[n].key = "bar1"; + kv[n].value = v[n]; + n++; + if (st->have_timer) { + time_t secs = (time_t)(st->timer_nsec / 1000000000ULL); + + gmtime_r(&secs, &tm); + strftime(when, sizeof(when), "%Y-%m-%dT%H:%M:%S", &tm); + snprintf(v[n], sizeof(v[n]), "%llu ns (%s.%09lluZ)", + (unsigned long long)st->timer_nsec, when, + (unsigned long long)(st->timer_nsec % 1000000000ULL)); + kv[n].key = "gpu_timer"; + kv[n].value = v[n]; + n++; + } + { + size_t off = 0; + unsigned int i; + + v[n][0] = '\0'; + for (i = 0; i < st->engine_count && off < sizeof(v[n]) - 2; + i++) { + const char *name = st->engine_names_ok ? + st->engine_names[i] : "engine"; + + off += (size_t)snprintf(v[n] + off, + sizeof(v[n]) - off, "%s%s", + i ? " " : "", name); + } + kv[n].key = "engines"; + kv[n].value = v[n][0] ? v[n] : "-"; + n++; + } + kv[n].key = "live_util_power_thermal"; + kv[n].value = "n/a (read-only minimal build; see docs/17)"; + n++; + rm_print_kv("rmtop", kv, n, format); +} -- cgit v1.2.3