gfursin commited on
Commit
d62f6c4
·
unverified ·
1 Parent(s): 4d2d78e

Fixed outdated functions to keep this prototype working on HuggingFace

Browse files
Files changed (6) hide show
  1. .python-version +1 -1
  2. CLAUDE.md +125 -0
  3. README.md +7 -3
  4. app.py +3 -4
  5. predictor.py +21 -4
  6. requirements.txt +1 -1
.python-version CHANGED
@@ -1 +1 @@
1
- 3.12
 
1
+ 3.14
CLAUDE.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ Author: Grigori Fursin (cTuning Labs)
6
+
7
+ ## What this is
8
+
9
+ FlexBoard is a Gradio web app that helps users find optimal AI-inference hardware
10
+ configurations from FlexBench/MLPerf benchmark results. Given a workload spec (model
11
+ architecture, size, precision) and hardware constraints, it filters real benchmark data,
12
+ optionally augments it with ML-predicted hypothetical configurations, ranks results by
13
+ performance or cost, and visualizes model-prediction quality. It is deployed as a Hugging
14
+ Face Space (see the YAML front matter in `README.md`).
15
+
16
+ The whole app is a single `gr.Blocks` interface built in `app.py`; there is no separate
17
+ frontend or server layer.
18
+
19
+ ## Running
20
+
21
+ The `.bat` files reflect the actual local dev workflow (uv + Python 3.12 venv):
22
+
23
+ ```bash
24
+ # 1. create venv
25
+ uv venv --python 3.12 .venv
26
+ # 2. install deps (note: also force-reinstalls a LOCAL cMeta checkout — see below)
27
+ uv pip install -r requirements.txt
28
+ # 3. run
29
+ uv run python app.py # launches Gradio on the default local port
30
+ ```
31
+
32
+ `_2_install_deps.bat` force-reinstalls cMeta from a hardcoded local path
33
+ (`D:\!FGG_Repos\fgg\fgg.project\cMeta\cmeta[all]`). This path is machine-specific to the
34
+ maintainer and will not exist elsewhere; on other machines install `cmind`/cMeta from its
35
+ normal source or skip it if the data pipeline isn't being touched. The README's simpler
36
+ `pip install -r requirements.txt` + `python -m app` also works if you don't need cMeta.
37
+
38
+ There are **no tests, linter config, or build step** in this repo.
39
+
40
+ ## Data
41
+
42
+ `data.json` (~847 records, git-LFS-tracked large file) is the sole input — pre-processed
43
+ FlexBench/MLPerf results with **dot-namespaced flat keys** (e.g. `metrics.result`,
44
+ `system.accelerator.name`, `model.number_of_parameters`). This flat dotted-key convention
45
+ is load-bearing: feature definitions, filtering, and prediction all key off these exact
46
+ strings. `utils.load_data()` reads the JSON, coerces numeric-looking strings to int/float,
47
+ and returns a **Polars** DataFrame.
48
+
49
+ Note the two-DataFrame split throughout the app:
50
+ - **Polars** `df` — used only for `extract_metadata()` (building UI dropdown/slider choices).
51
+ - **pandas** `pd_df` — used by everything else (`predictor.py`, `recommender.py`,
52
+ `cost_calculator.py`, all filtering in `app.py`). When adding logic, match the library
53
+ already in use in that module.
54
+
55
+ ## Architecture / data flow
56
+
57
+ `utils.py` is the schema authority. `FEATURES` maps every column to a group and a type
58
+ (`continuous` / `categorical` / `boolean` / `text`); `FEATURE_TYPES` and `UI_FEATURE_GROUPS`
59
+ are derived from it. `get_feature_type()` drives whether a filter does exact-match
60
+ (categorical) or ±tolerance range-match (continuous). Adding/renaming a data column means
61
+ updating `FEATURES` here first.
62
+
63
+ At startup `app.py` loads data once into module globals (`df`, `pd_df`, `metadata`,
64
+ `predictor`, `config_finder`) — these are shared, not per-session. It then defines all
65
+ Gradio components and wires callbacks inside one `gr.Blocks` context.
66
+
67
+ Request flow when the user clicks **Search Configurations**:
68
+ 1. `process_framework_inputs(*args)` unpacks the flat positional args (order defined by
69
+ `all_inputs` + framework dropdowns — **keep these lists in sync with the callback's
70
+ index-based unpacking**, e.g. `base_args[16]` etc.) into `workload_specs` and
71
+ `constraints` dicts.
72
+ 2. `find_best_configs()` filters `pd_df`: exact-match for categoricals, ±10% tolerance for
73
+ continuous features (`apply_continuous_feature_tolerance`), plus explicit min/max
74
+ range filters for memory and accelerator count.
75
+ 3. If predictions are enabled and architecture+model_size are set,
76
+ `predictor.generate_predictions()` synthesizes hypothetical configs; these are
77
+ cost-scored and concatenated with real results, tagged via a `predicted` boolean column.
78
+ 4. Results are ranked by `metrics.result_per_accelerator` (performance) or
79
+ `cost_per_million_tokens` (cost), then formatted for the three output tabs and the bar chart.
80
+
81
+ The `predicted` column and `system.name = "Hypothetical system - ongoing work"` are how
82
+ generated rows are distinguished from real benchmark rows downstream.
83
+
84
+ ### The predictor (`predictor.py`)
85
+
86
+ `PerformancePredictor` trains an **XGBoost regressor** (with `enable_categorical=True`, so
87
+ object columns are cast to pandas `category` dtype rather than one-hot encoded) on
88
+ `data.json` at construction, targeting `metrics.result_per_accelerator`. It excludes
89
+ leakage-prone columns (`submission.*`, all `metrics.*`, `model.name`, `system.name`, etc.).
90
+
91
+ Beyond prediction it does **statistical data synthesis**: `_analyze_data_distributions()`
92
+ and the `_analyze_*_relations()` methods build conditional distributions (vendor→accelerator,
93
+ accelerator→memory, vendor→software stack, node→device-count, …). `_generate_configs()`
94
+ samples from these to produce *plausible* hardware configs respecting user constraints, which
95
+ the model then scores. This is why predictions look realistic rather than random. Evaluation
96
+ metrics (RMSE/MAE/R²/MAPE), a held-out test set, and feature importances are computed in
97
+ `_evaluate_model()` and surfaced in the "ML Model Performance" tab.
98
+
99
+ ### Cost model (`cost_calculator.py`)
100
+
101
+ Uses **module-global mutable state** `device_costs`, seeded from `DEFAULT_DEVICE_COSTS` by
102
+ `initialize_device_costs()`. `normalize_gpu_name()` collapses raw accelerator names into
103
+ device families (e.g. any "H100" → "NVIDIA H100"). The "Device Cost Settings" tab lets users
104
+ edit hourly costs live, mutating this global. `cost_per_million_tokens` is derived as
105
+ `hourly_cost / (result_per_accelerator * 3600) * 1e6`.
106
+
107
+ ### Recommender (`recommender.py`)
108
+
109
+ `ConfigurationFinder` is a **separate, simpler filtering/ranking path** than
110
+ `find_best_configs()` in `app.py`. It's instantiated as `config_finder` but the main
111
+ search UI currently routes through `app.py`'s own logic; keep this in mind before assuming
112
+ `recommender.py` is on the hot path.
113
+
114
+ ## Conventions & gotchas
115
+
116
+ - **Column names are string literals everywhere.** There is no central enum beyond
117
+ `FEATURES` in `utils.py`; renaming a column requires a repo-wide search for the dotted string.
118
+ - Framework columns are dynamic: any `software.framework.<name>` column becomes a UI dropdown
119
+ automatically via `extract_metadata()`. Adding a framework to the data adds a filter with
120
+ no code change.
121
+ - Gradio callbacks pass inputs **positionally**. `process_framework_inputs` and
122
+ `get_constraints_from_args` index into `*args` by hardcoded position — changing the
123
+ `all_inputs` list order will silently break constraint mapping.
124
+ - `±10% tolerance` on continuous features is intentional app behavior (stated in the UI), not
125
+ a bug — see `apply_continuous_feature_tolerance` and `ConfigurationFinder.is_within_tolerance`.
README.md CHANGED
@@ -15,6 +15,11 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
15
 
16
  # FlexBoard
17
 
 
 
 
 
 
18
  ## Installation
19
 
20
  ```bash
@@ -31,13 +36,12 @@ pip install -r requirements.txt
31
  python -m app
32
  ```
33
 
34
-
35
  ## License and Copyright
36
 
37
  This project is licensed under the [Apache License 2.0](LICENSE.md).
38
 
39
  © 2025 FlexAI
40
 
41
- ## Authors and maintaners
42
 
43
- [Daniel Altunay](https://www.linkedin.com/in/daltunay) and [Grigori Fursin](https://cKnowledge.org/gfursin) (FCS Labs)
 
15
 
16
  # FlexBoard
17
 
18
+ ## Status
19
+
20
+ This project is an **archived prototype**. Further development continues at
21
+ [cTuning Labs](https://cTuning.ai).
22
+
23
  ## Installation
24
 
25
  ```bash
 
36
  python -m app
37
  ```
38
 
 
39
  ## License and Copyright
40
 
41
  This project is licensed under the [Apache License 2.0](LICENSE.md).
42
 
43
  © 2025 FlexAI
44
 
45
+ ## Authors
46
 
47
+ [Daniel Altunay](https://www.linkedin.com/in/daltunay) and [Grigori Fursin](https://cKnowledge.org/gfursin)
app.py CHANGED
@@ -845,10 +845,10 @@ def create_model_performance_plot(
845
  return fig, metrics, feature_importance.head(10)
846
 
847
 
848
- with gr.Blocks(title="MLPerf Configuration Finder") as interface:
849
  gr.Markdown(
850
  """
851
- # 🔍 MLPerf Configuration Finder (ongoing preliminary work)
852
 
853
  Find the optimal configurations for your AI workloads by specifying your model and constraints.
854
  Results are ranked by performance and include both real benchmark data and AI-generated predictions.
@@ -1055,7 +1055,6 @@ with gr.Blocks(title="MLPerf Configuration Finder") as interface:
1055
  col_count=(2, "fixed"),
1056
  interactive=True,
1057
  wrap=True,
1058
- show_copy_button=True,
1059
  show_search="filter",
1060
  )
1061
 
@@ -1437,7 +1436,7 @@ with gr.Blocks(title="MLPerf Configuration Finder") as interface:
1437
  gr.HTML("""
1438
  <div style="text-align: center;">
1439
  Authors: <a href="https://www.linkedin.com/in/daltunay">Daniel Altunay</a> and
1440
- <a href="https://cKnowledge.org/gfursin">Grigori Fursin</a> (FCS Labs)
1441
  </div>
1442
  """)
1443
 
 
845
  return fig, metrics, feature_importance.head(10)
846
 
847
 
848
+ with gr.Blocks(title="MLPerf Configuration Finder (prototype archive)") as interface:
849
  gr.Markdown(
850
  """
851
+ # 🔍 MLPerf Configuration Finder (prototype archive)
852
 
853
  Find the optimal configurations for your AI workloads by specifying your model and constraints.
854
  Results are ranked by performance and include both real benchmark data and AI-generated predictions.
 
1055
  col_count=(2, "fixed"),
1056
  interactive=True,
1057
  wrap=True,
 
1058
  show_search="filter",
1059
  )
1060
 
 
1436
  gr.HTML("""
1437
  <div style="text-align: center;">
1438
  Authors: <a href="https://www.linkedin.com/in/daltunay">Daniel Altunay</a> and
1439
+ <a href="https://cTuning.ai/@gfursin">Grigori Fursin</a>
1440
  </div>
1441
  """)
1442
 
predictor.py CHANGED
@@ -28,6 +28,7 @@ class PerformancePredictor:
28
  self.evaluation_data = pd.DataFrame()
29
  self.evaluation_metrics = {}
30
  self.feature_importance = pd.DataFrame(columns=["Feature", "Importance"])
 
31
 
32
  self.excluded_features = {
33
  "model.name",
@@ -102,7 +103,10 @@ class PerformancePredictor:
102
  }
103
 
104
  for feature in continuous_features:
105
- values = self.df[feature].dropna()
 
 
 
106
  if len(values) > 0:
107
  self.distributions[feature] = {
108
  "min": float(values.min()),
@@ -283,6 +287,13 @@ class PerformancePredictor:
283
  with pd.option_context("mode.chained_assignment", None):
284
  X[col] = X[col].astype("category")
285
 
 
 
 
 
 
 
 
286
  try:
287
  strat_column = df_clean["system.accelerator.name"].fillna("unknown")
288
  X_train, X_test, y_train, y_test = train_test_split(
@@ -421,9 +432,15 @@ class PerformancePredictor:
421
  if feature not in configs_df.columns:
422
  configs_df[feature] = None
423
 
424
- X_pred = configs_df[model_features]
425
- for col in X_pred.select_dtypes(include=["object"]).columns:
426
- with pd.option_context("mode.chained_assignment", None):
 
 
 
 
 
 
427
  X_pred[col] = X_pred[col].astype("category")
428
 
429
  configs_df[self.target] = self.model.predict(X_pred)
 
28
  self.evaluation_data = pd.DataFrame()
29
  self.evaluation_metrics = {}
30
  self.feature_importance = pd.DataFrame(columns=["Feature", "Importance"])
31
+ self.feature_dtypes = {}
32
 
33
  self.excluded_features = {
34
  "model.name",
 
103
  }
104
 
105
  for feature in continuous_features:
106
+ # Coerce to numeric so stray non-numeric entries (e.g. "-") become
107
+ # NaN and are dropped, rather than forcing the whole column to string
108
+ # and breaking float(values.min()) below.
109
+ values = pd.to_numeric(self.df[feature], errors="coerce").dropna()
110
  if len(values) > 0:
111
  self.distributions[feature] = {
112
  "min": float(values.min()),
 
287
  with pd.option_context("mode.chained_assignment", None):
288
  X[col] = X[col].astype("category")
289
 
290
+ # Remember the exact column dtypes (incl. categorical categories) used
291
+ # for training so prediction inputs can be aligned to them. This keeps
292
+ # categorical columns non-empty even when a generated config leaves a
293
+ # feature entirely null, which newer xgboost/numpy would otherwise
294
+ # reject ("cannot call vectorize on size 0 inputs").
295
+ self.feature_dtypes = X.dtypes.to_dict()
296
+
297
  try:
298
  strat_column = df_clean["system.accelerator.name"].fillna("unknown")
299
  X_train, X_test, y_train, y_test = train_test_split(
 
432
  if feature not in configs_df.columns:
433
  configs_df[feature] = None
434
 
435
+ X_pred = configs_df[model_features].copy()
436
+ for col in model_features:
437
+ train_dtype = self.feature_dtypes.get(col)
438
+ if isinstance(train_dtype, pd.CategoricalDtype):
439
+ # Reuse the training categories so all-null generated columns
440
+ # still carry a non-empty category set (xgboost's categorical
441
+ # path errors on zero-category columns).
442
+ X_pred[col] = X_pred[col].astype(train_dtype)
443
+ elif X_pred[col].dtype == object:
444
  X_pred[col] = X_pred[col].astype("category")
445
 
446
  configs_df[self.target] = self.model.predict(X_pred)
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
  datasets
2
- gradio
3
  nbformat
4
  numpy
5
  pandas
 
1
  datasets
2
+ gradio<6
3
  nbformat
4
  numpy
5
  pandas