FlexBoard / CLAUDE.md
gfursin's picture
Fixed outdated functions to keep this prototype working on HuggingFace
d62f6c4 unverified
|
Raw
History Blame Contribute Delete
6.91 kB
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Author: Grigori Fursin (cTuning Labs)
## What this is
FlexBoard is a Gradio web app that helps users find optimal AI-inference hardware
configurations from FlexBench/MLPerf benchmark results. Given a workload spec (model
architecture, size, precision) and hardware constraints, it filters real benchmark data,
optionally augments it with ML-predicted hypothetical configurations, ranks results by
performance or cost, and visualizes model-prediction quality. It is deployed as a Hugging
Face Space (see the YAML front matter in `README.md`).
The whole app is a single `gr.Blocks` interface built in `app.py`; there is no separate
frontend or server layer.
## Running
The `.bat` files reflect the actual local dev workflow (uv + Python 3.12 venv):
```bash
# 1. create venv
uv venv --python 3.12 .venv
# 2. install deps (note: also force-reinstalls a LOCAL cMeta checkout — see below)
uv pip install -r requirements.txt
# 3. run
uv run python app.py # launches Gradio on the default local port
```
`_2_install_deps.bat` force-reinstalls cMeta from a hardcoded local path
(`D:\!FGG_Repos\fgg\fgg.project\cMeta\cmeta[all]`). This path is machine-specific to the
maintainer and will not exist elsewhere; on other machines install `cmind`/cMeta from its
normal source or skip it if the data pipeline isn't being touched. The README's simpler
`pip install -r requirements.txt` + `python -m app` also works if you don't need cMeta.
There are **no tests, linter config, or build step** in this repo.
## Data
`data.json` (~847 records, git-LFS-tracked large file) is the sole input — pre-processed
FlexBench/MLPerf results with **dot-namespaced flat keys** (e.g. `metrics.result`,
`system.accelerator.name`, `model.number_of_parameters`). This flat dotted-key convention
is load-bearing: feature definitions, filtering, and prediction all key off these exact
strings. `utils.load_data()` reads the JSON, coerces numeric-looking strings to int/float,
and returns a **Polars** DataFrame.
Note the two-DataFrame split throughout the app:
- **Polars** `df` — used only for `extract_metadata()` (building UI dropdown/slider choices).
- **pandas** `pd_df` — used by everything else (`predictor.py`, `recommender.py`,
`cost_calculator.py`, all filtering in `app.py`). When adding logic, match the library
already in use in that module.
## Architecture / data flow
`utils.py` is the schema authority. `FEATURES` maps every column to a group and a type
(`continuous` / `categorical` / `boolean` / `text`); `FEATURE_TYPES` and `UI_FEATURE_GROUPS`
are derived from it. `get_feature_type()` drives whether a filter does exact-match
(categorical) or ±tolerance range-match (continuous). Adding/renaming a data column means
updating `FEATURES` here first.
At startup `app.py` loads data once into module globals (`df`, `pd_df`, `metadata`,
`predictor`, `config_finder`) — these are shared, not per-session. It then defines all
Gradio components and wires callbacks inside one `gr.Blocks` context.
Request flow when the user clicks **Search Configurations**:
1. `process_framework_inputs(*args)` unpacks the flat positional args (order defined by
`all_inputs` + framework dropdowns — **keep these lists in sync with the callback's
index-based unpacking**, e.g. `base_args[16]` etc.) into `workload_specs` and
`constraints` dicts.
2. `find_best_configs()` filters `pd_df`: exact-match for categoricals, ±10% tolerance for
continuous features (`apply_continuous_feature_tolerance`), plus explicit min/max
range filters for memory and accelerator count.
3. If predictions are enabled and architecture+model_size are set,
`predictor.generate_predictions()` synthesizes hypothetical configs; these are
cost-scored and concatenated with real results, tagged via a `predicted` boolean column.
4. Results are ranked by `metrics.result_per_accelerator` (performance) or
`cost_per_million_tokens` (cost), then formatted for the three output tabs and the bar chart.
The `predicted` column and `system.name = "Hypothetical system - ongoing work"` are how
generated rows are distinguished from real benchmark rows downstream.
### The predictor (`predictor.py`)
`PerformancePredictor` trains an **XGBoost regressor** (with `enable_categorical=True`, so
object columns are cast to pandas `category` dtype rather than one-hot encoded) on
`data.json` at construction, targeting `metrics.result_per_accelerator`. It excludes
leakage-prone columns (`submission.*`, all `metrics.*`, `model.name`, `system.name`, etc.).
Beyond prediction it does **statistical data synthesis**: `_analyze_data_distributions()`
and the `_analyze_*_relations()` methods build conditional distributions (vendor→accelerator,
accelerator→memory, vendor→software stack, node→device-count, …). `_generate_configs()`
samples from these to produce *plausible* hardware configs respecting user constraints, which
the model then scores. This is why predictions look realistic rather than random. Evaluation
metrics (RMSE/MAE/R²/MAPE), a held-out test set, and feature importances are computed in
`_evaluate_model()` and surfaced in the "ML Model Performance" tab.
### Cost model (`cost_calculator.py`)
Uses **module-global mutable state** `device_costs`, seeded from `DEFAULT_DEVICE_COSTS` by
`initialize_device_costs()`. `normalize_gpu_name()` collapses raw accelerator names into
device families (e.g. any "H100" → "NVIDIA H100"). The "Device Cost Settings" tab lets users
edit hourly costs live, mutating this global. `cost_per_million_tokens` is derived as
`hourly_cost / (result_per_accelerator * 3600) * 1e6`.
### Recommender (`recommender.py`)
`ConfigurationFinder` is a **separate, simpler filtering/ranking path** than
`find_best_configs()` in `app.py`. It's instantiated as `config_finder` but the main
search UI currently routes through `app.py`'s own logic; keep this in mind before assuming
`recommender.py` is on the hot path.
## Conventions & gotchas
- **Column names are string literals everywhere.** There is no central enum beyond
`FEATURES` in `utils.py`; renaming a column requires a repo-wide search for the dotted string.
- Framework columns are dynamic: any `software.framework.<name>` column becomes a UI dropdown
automatically via `extract_metadata()`. Adding a framework to the data adds a filter with
no code change.
- Gradio callbacks pass inputs **positionally**. `process_framework_inputs` and
`get_constraints_from_args` index into `*args` by hardcoded position — changing the
`all_inputs` list order will silently break constraint mapping.
- `±10% tolerance` on continuous features is intentional app behavior (stated in the UI), not
a bug — see `apply_continuous_feature_tolerance` and `ConfigurationFinder.is_within_tolerance`.