lee101 commited on
Commit
d5ef81d
·
verified ·
1 Parent(s): 379fe7d

mojo-tabular: tiny CPU tabular classifiers

Browse files
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ artifacts/
2
+ artifacts-gb/
3
+ __pycache__/
4
+ *.pyc
5
+ .env*
6
+ *.pem
7
+ *.key
8
+ prompts/
9
+ REVIEW.md
10
+ cog/.cog/
README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: scikit-learn
4
+ pipeline_tag: tabular-classification
5
+ tags: [tabular, cpu, tiny, cog, app-nz]
6
+ ---
7
+
8
+ # mojo-tabular
9
+
10
+ Tiny CPU tabular classifiers (one per dataset), total 12 MB, no GPU. Trained with scikit-learn;
11
+ `mojo-` is the planned optimization path, not a Mojo implementation yet.
12
+
13
+ ## Results (ROC-AUC, seed 42, 60/20/20 stratified split, selection on validation only)
14
+
15
+ | dataset (OpenML) | val | test | untuned best baseline test | MB |
16
+ |---|---|---|---|---|
17
+ | adult | 0.929 | 0.9282 | 0.9272 | 2.64 |
18
+ | credit-g | 0.8294 | 0.7986 | 0.7958 | 0.00 |
19
+ | bank-marketing | 0.9354 | 0.9384 | 0.9375 | 2.67 |
20
+ | phoneme | 0.9523 | 0.9546 | 0.9554 | 5.50 |
21
+ | diabetes | 0.8528 | 0.8001 | 0.8031 | 1.09 |
22
+
23
+ Search: 16 random HistGradientBoosting configs + logistic regression + extra trees; top-3 averaged
24
+ when validation improves. Single split, single seed: small sets (credit-g n=1000, diabetes n=768) have
25
+ large validation/test gaps and are noisy. No comparison against XGBoost/CatBoost/TabPFN has been run,
26
+ so **no frontier claim is made**. Adding tuned XGBoost 3.4.1 and LightGBM 4.7.0 (6 configs each) to the pool changed test AUC by at most +0.001 (adult 0.929, bank-marketing 0.9386), i.e. these results sit at the plateau of strong boosted trees; the shipped models use sklearn only to keep the image small. CatBoost and TabPFN were not compared (CatBoost hung under the 4 GiB address-space limit). Data: public OpenML sets (license listed as Public).
27
+ Reproduce: `train/fetch_tabular.py`, `train/tabular_tune.py` (`--gbdt` needs xgboost/lightgbm), artifact hashes in `results/tuned.json`.
28
+
29
+ ## Use
30
+
31
+ ```python
32
+ import joblib, pandas as pd
33
+ b = joblib.load("models/adult.joblib") # needs scikit-learn==1.8.0
34
+ p = sum(m.predict_proba(df[b["columns"]])[:, 1] for m in b["members"]) / len(b["members"])
35
+ ```
36
+
37
+ ## Deploy on app.nz (Cog, CPU)
38
+
39
+ ```bash
40
+ cd cog && python3 -m unittest discover -s tests # contract test, no network
41
+ docker build -t ghcr.io/lee101/mojo-tabular:latest . # 381 MB (python:3.12-slim), weights baked
42
+ docker run --rm -p 5000:5000 --memory 512m ghcr.io/lee101/mojo-tabular:latest # ~80 MB RSS, ready in <10 s
43
+ ```
44
+
45
+ [![Deploy to app.nz](https://app.nz/deploy-button.svg)](https://app.nz/deploy?image=ghcr.io/lee101/mojo-tabular:latest&name=mojo-tabular&hardware=cpu&idleSeconds=60)
46
+
47
+ `POST /predictions {"input": {"dataset": "adult", "rows": "[{...}]"}}` returns a JSON list of probabilities
48
+ (max 1000 rows). Do not use the default `cog build` image: its apt layer alone is 1.78 GB (2.33 GB total).
49
+ Links: GitHub `lee101/mojo-tabular`, Hugging Face `lee101/mojo-tabular`.
50
+
51
+ ## Limitations
52
+
53
+ Trained on public benchmark data only; not validated for credit, hiring or medical decisions.
artifacts/.gitkeep ADDED
File without changes
artifacts/adult.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a28557fc9992cefa1eaaf2308964baa93b1c7e78f266c1a349fd64cdb1b246a7
3
+ size 2639714
artifacts/bank-marketing.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec72f430d51f216a15d3a611e6ff1ed279ecc022759160354cb821a6077f29ea
3
+ size 2670481
artifacts/credit-g.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:240366785f1e95fa3749b741747be6d4bb258d5be362e03cc5dec768b10d0496
3
+ size 3369
artifacts/diabetes.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:50b5ecc4afc5e291dc45c7ce504a40fe2d5f735d3c86157a6c10a95407a9691f
3
+ size 1085047
artifacts/phoneme.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e37ac3db2ba7bd9839833d8e0ac8a0a537467ddcf58b098d6550af47485fd683
3
+ size 5496319
cog/.cog/cog.lock ADDED
File without changes
cog/.cog/openapi_schema.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"components":{"schemas":{"Input":{"properties":{"dataset":{"title":"Dataset","type":"string","x-order":0},"rows":{"title":"Rows","type":"string","x-order":1}},"required":["dataset","rows"],"title":"Input","type":"object"},"Output":{"title":"Output","type":"string"},"PredictionRequest":{"properties":{"id":{"title":"Id","type":"string"},"input":{"$ref":"#/components/schemas/Input"}},"title":"PredictionRequest","type":"object"},"PredictionResponse":{"properties":{"completed_at":{"format":"date-time","title":"Completed At","type":"string"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"error":{"title":"Error","type":"string"},"id":{"title":"Id","type":"string"},"input":{"$ref":"#/components/schemas/Input"},"logs":{"title":"Logs","type":"string"},"metrics":{"title":"Metrics","type":"object"},"output":{"$ref":"#/components/schemas/Output"},"started_at":{"format":"date-time","title":"Started At","type":"string"},"status":{"title":"Status","type":"string"},"version":{"title":"Version","type":"string"}},"title":"PredictionResponse","type":"object"},"Status":{"description":"An enumeration.","enum":["starting","processing","succeeded","canceled","failed"],"title":"Status","type":"string"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"info":{"title":"Cog","version":"0.1.0"},"openapi":"3.0.2","paths":{"/":{"get":{"operationId":"root__get","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"}},"summary":"Root"}},"/health-check":{"get":{"operationId":"healthcheck_health_check_get","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"}},"summary":"Healthcheck"}},"/predictions":{"post":{"description":"Run a single prediction on the model","operationId":"predict_predictions_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PredictionRequest"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PredictionResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Predict"}},"/predictions/{prediction_id}/cancel":{"post":{"operationId":"cancel_predictions__prediction_id__cancel_post","parameters":[{"in":"path","name":"prediction_id","required":true,"schema":{"title":"Prediction Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Cancel"}}}}
cog/.dockerignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ tests
2
+ __pycache__
3
+ *.md
cog/Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 OMP_NUM_THREADS=1
3
+ WORKDIR /app
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt && find /usr/local -name '__pycache__' -prune -exec rm -rf {} +
6
+ COPY predict.py server.py ./
7
+ COPY models models
8
+ USER nobody
9
+ EXPOSE 5000
10
+ CMD ["python", "server.py"]
cog/models/adult.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a28557fc9992cefa1eaaf2308964baa93b1c7e78f266c1a349fd64cdb1b246a7
3
+ size 2639714
cog/models/bank-marketing.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec72f430d51f216a15d3a611e6ff1ed279ecc022759160354cb821a6077f29ea
3
+ size 2670481
cog/models/credit-g.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:240366785f1e95fa3749b741747be6d4bb258d5be362e03cc5dec768b10d0496
3
+ size 3369
cog/models/diabetes.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:50b5ecc4afc5e291dc45c7ce504a40fe2d5f735d3c86157a6c10a95407a9691f
3
+ size 1085047
cog/models/phoneme.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e37ac3db2ba7bd9839833d8e0ac8a0a537467ddcf58b098d6550af47485fd683
3
+ size 5496319
cog/predict.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ import joblib
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ MODELS = Path(__file__).parent / 'models'
8
+ MAX_ROWS = 1000
9
+
10
+
11
+ class Predictor:
12
+ def setup(self):
13
+ self.models = {p.stem: joblib.load(p) for p in sorted(MODELS.glob('*.joblib'))}
14
+
15
+ def predict(self, dataset: str, rows: str) -> str:
16
+ if dataset not in self.models:
17
+ raise ValueError(f'dataset must be one of {sorted(self.models)}')
18
+ data = json.loads(rows)
19
+ if isinstance(data, dict):
20
+ data = [data]
21
+ if not isinstance(data, list) or not data or len(data) > MAX_ROWS:
22
+ raise ValueError(f'rows must be 1..{MAX_ROWS} JSON objects')
23
+ bundle = self.models[dataset]
24
+ missing = [c for c in bundle['columns'] if c not in data[0]]
25
+ if missing:
26
+ raise ValueError(f'missing columns: {missing}')
27
+ df = pd.DataFrame(data).reindex(columns=bundle['columns'])
28
+ p = np.mean([m.predict_proba(df)[:, 1] for m in bundle['members']], axis=0)
29
+ return json.dumps([round(float(x), 6) for x in p])
cog/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ scikit-learn==1.8.0
2
+ joblib==1.5.3
3
+ numpy==2.3.5
4
+ pandas==3.0.1
cog/server.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
3
+ from predict import Predictor
4
+
5
+ MAX_BODY = 4 * 1024 * 1024
6
+ P = Predictor()
7
+ STATE = {'status': 'SETUP'}
8
+ SCHEMA = {'outputKind': 'json', 'inputs': [
9
+ {'name': 'dataset', 'type': 'string', 'required': True, 'order': 0,
10
+ 'choices': ['adult', 'bank-marketing', 'credit-g', 'diabetes', 'phoneme'],
11
+ 'description': 'Model to use (OpenML dataset schema)'},
12
+ {'name': 'rows', 'type': 'string', 'required': True, 'order': 1,
13
+ 'description': 'JSON object or list of up to 1000 objects with the dataset columns'}]}
14
+
15
+
16
+ class H(BaseHTTPRequestHandler):
17
+ def send(self, code, obj):
18
+ b = json.dumps(obj).encode()
19
+ self.send_response(code)
20
+ self.send_header('content-type', 'application/json')
21
+ self.send_header('content-length', str(len(b)))
22
+ self.end_headers()
23
+ self.wfile.write(b)
24
+
25
+ def do_GET(self):
26
+ if self.path == '/health-check':
27
+ self.send(200, {'status': STATE['status']})
28
+ elif self.path == '/openapi.json':
29
+ self.send(200, SCHEMA)
30
+ else:
31
+ self.send(404, {'error': 'not found'})
32
+
33
+ def do_POST(self):
34
+ if self.path != '/predictions':
35
+ return self.send(404, {'error': 'not found'})
36
+ n = int(self.headers.get('content-length') or 0)
37
+ if n > MAX_BODY:
38
+ return self.send(413, {'status': 'failed', 'error': 'body too large'})
39
+ try:
40
+ inp = json.loads(self.rfile.read(n))['input']
41
+ out = P.predict(inp['dataset'], inp['rows'])
42
+ self.send(200, {'status': 'succeeded', 'output': json.loads(out)})
43
+ except Exception as e:
44
+ self.send(200, {'status': 'failed', 'error': f'{type(e).__name__}: {e}'[:300]})
45
+
46
+ def log_message(self, *a):
47
+ pass
48
+
49
+
50
+ if __name__ == '__main__':
51
+ P.setup()
52
+ STATE['status'] = 'READY'
53
+ ThreadingHTTPServer(('0.0.0.0', 5000), H).serve_forever()
cog/tests/test_predict.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, sys, unittest
2
+ from pathlib import Path
3
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
4
+ import pandas as pd
5
+ from predict import Predictor
6
+
7
+ DATA = Path('/nvme0n1-disk/cache/foundry/tabular')
8
+
9
+
10
+ class T(unittest.TestCase):
11
+ @classmethod
12
+ def setUpClass(cls):
13
+ cls.p = Predictor()
14
+ cls.p.setup()
15
+
16
+ def test_all_models(self):
17
+ for name, target in [('adult', 'class'), ('credit-g', 'class'), ('bank-marketing', 'Class'),
18
+ ('phoneme', 'Class'), ('diabetes', 'class')]:
19
+ f = DATA / f'{name}.parquet'
20
+ if not f.exists():
21
+ self.skipTest('dataset cache absent')
22
+ df = pd.read_parquet(f).drop(columns=[target]).head(5)
23
+ rows = df.astype(object).where(df.notna(), None).to_json(orient='records')
24
+ out = json.loads(self.p.predict(name, rows))
25
+ self.assertEqual(len(out), 5)
26
+ self.assertTrue(all(0 <= x <= 1 for x in out))
27
+
28
+ def test_errors(self):
29
+ with self.assertRaises(ValueError):
30
+ self.p.predict('nope', '[]')
31
+ with self.assertRaises(ValueError):
32
+ self.p.predict('adult', '[{"age": 1}]')
33
+
34
+
35
+ if __name__ == '__main__':
36
+ unittest.main()
configs/.gitkeep ADDED
File without changes
configs/project.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mojo-tabular",
3
+ "task": "Structured-data classification and regression",
4
+ "priority": 1,
5
+ "status": "scaffold",
6
+ "backend": "pytorch-first; mojo after parity tests",
7
+ "max_parameters": 50000000,
8
+ "device": "cpu",
9
+ "dataset": null,
10
+ "objective": null,
11
+ "hf_model": null,
12
+ "hf_revision": null,
13
+ "profiles": [
14
+ "tiny",
15
+ "fast",
16
+ "balanced",
17
+ "max",
18
+ "robust"
19
+ ]
20
+ }
prompts/.gitkeep ADDED
File without changes
prompts/implement.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Work only in projects/mojo-tabular. Read AGENTS.md and configs/project.json.
2
+ Task: Structured-data classification and regression. Implement ONE small, reviewable baseline or improvement, with tests.
3
+ First inspect existing code and state your assumptions. Do not invent benchmark results.
4
+ Do not download data/models, install packages, use a GPU, launch background jobs,
5
+ change resource limits, access credentials, publish, or edit other projects.
6
+ Use synthetic fixtures for tests. Keep the hidden test split untouched.
7
+ For an HF candidate, propose its ID, immutable revision, license, parameter count,
8
+ estimated RAM and dataset suitability; stop for review before any download.
9
+ Finish with SUMMARY: files changed, tests run and outcomes, limitations, next step.
prompts/kernel.txt ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CONTEXT
2
+ A small-model foundry at /sdb-disk/code/modelopt ports neural-network and
3
+ feature-transform inference kernels to Mojo. A pinned Mojo toolchain is
4
+ installed and verified working. An existing, parity-tested example of the
5
+ required pattern is in /sdb-disk/code/modelopt/src/kernels.mojo, its Python
6
+ bridge in /sdb-disk/code/modelopt/foundry/mojo_kernels.py and its tests in
7
+ /sdb-disk/code/modelopt/tests/test_kernels.py. READ ALL THREE FIRST and copy
8
+ their exact idioms. Do not invent Mojo syntax; a wrong guess wastes your steps.
9
+
10
+ VERIFIED MOJO RECIPE (Mojo 1.2.0.dev2026092605; all known-good)
11
+ - Build: export PATH=/nvme0n1-disk/code/mojo-numpy/.pixi/envs/default/bin:$PATH
12
+ export MODULAR_HOME=/nvme0n1-disk/code/mojo-numpy/.pixi/envs/default/share/max
13
+ mojo build --emit shared-lib <file>.mojo -o <out>.so
14
+ MODULAR_HOME is mandatory or every import fails with "unable to locate module 'std'".
15
+ - `--emit shared-lib` is required; a bare `mojo build` demands a main() and fails.
16
+ - `def` only; `fn` is removed. `comptime W = simd_width_of[DType.float32]()`.
17
+ - Pointer from an integer address: `comptime F32 = UnsafePointer[Float32, AnyOrigin[mut=True]]`
18
+ then `F32(unsafe_from_address=addr)`.
19
+ - @export REQUIRES an explicit abi effect: `@export("name")` on the line above
20
+ `def f(a_addr: Int) abi("C"):`. Pass buffer addresses as Int, never as pointers.
21
+ - SIMD: `acc += w.unsafe_load[width=W](i) * x.unsafe_load[width=W](i)`, then
22
+ `total = acc.reduce_add()` plus a scalar tail loop.
23
+ - Widening cast int8->float32 works as SIMD[DType.float32, W](int8_simd_value).
24
+ - Traps: SIMD[DType.float32, W](bias[o]) SPLATS into every lane so reduce_add()
25
+ counts the bias W times; add bias to the scalar total instead. `fn` is gone.
26
+ Multi-thread Mojo builtins used by other repos turned out to be serial stubs.
27
+
28
+ YOUR TASK
29
+ Create mojo-tabular/src/kernels.mojo (do NOT edit the shared
30
+ /sdb-disk/code/modelopt/src/kernels.mojo; another agent owns it) implementing:
31
+ 1. quantile_rank - map each row's values to their within-row rank scaled to [0,1]
32
+ 2. histogram_bin - bin each value into one of `bins` equal-width buckets, with out-of-range values clamped
33
+ 3. gini_impurity - Gini impurity of a class-count vector, returning 0.0 for an empty vector
34
+ Also create mojo-tabular/src/bridge.py (ctypes bridge that enforces float32
35
+ C-contiguous inputs and raises rather than silently narrowing) and
36
+ mojo-tabular/tests/test_kernels.py comparing EVERY kernel against a NumPy
37
+ reference. Cover sizes that are not multiples of the SIMD width, and any
38
+ degenerate case noted above.
39
+
40
+ HARD RULES
41
+ - CPU only. No GPU, no downloads, no package installs, no network access.
42
+ - Synthetic fixtures only. Never fabricate accuracy, throughput or benchmark numbers.
43
+ - Do not modify AGENTS.md, scripts/, pixi.toml, or any file outside mojo-tabular/.
44
+ - A kernel that does not match its NumPy reference is a FAILURE, not a caveat.
45
+ Actually run the tests and paste the verbatim output.
46
+
47
+ FINISH WITH
48
+ SUMMARY: files created, exact test command, verbatim pass/fail counts, measured
49
+ median latency for one realistic shape (or "Not measured"), known limitations,
50
+ and the single next step.
prompts/review.txt ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Review this small-model foundry scaffold. This is a text-only task: DO NOT call tools, read files, run commands, edit code, use the network, or access credentials. Use only the supplied excerpts. Return at most 450 words, ending with SUMMARY and the three highest-impact concrete improvements. Do not claim you ran tests. Focus on resource limitations, logging/token accounting, and repeatable experimentation. The scaffold contains 21 project dirs, PyTorch-first then Mojo parity ports; no real datasets/models selected. Six runner/catalog tests and a synthetic 577-parameter tabular smoke run passed.
2
+
3
+ #!/usr/bin/env python3
4
+ """Bound a local CPU task and retain its complete transcript."""
5
+ import argparse
6
+ import datetime
7
+ import fcntl
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import resource
12
+ import subprocess
13
+ import time
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+
17
+
18
+ def constrain():
19
+ os.nice(10)
20
+ cores = sorted(os.sched_getaffinity(0))[:2]
21
+ os.sched_setaffinity(0, cores)
22
+ resource.setrlimit(resource.RLIMIT_AS, (4 * 1024**3, 4 * 1024**3))
23
+ resource.setrlimit(resource.RLIMIT_FSIZE, (16 * 1024**2, 16 * 1024**2))
24
+ resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
25
+
26
+
27
+ def main():
28
+ parser = argparse.ArgumentParser(description=__doc__)
29
+ parser.add_argument('--seconds', type=int, default=120)
30
+ parser.add_argument('--cwd', type=Path, default=ROOT)
31
+ parser.add_argument('command', nargs=argparse.REMAINDER)
32
+ args = parser.parse_args()
33
+ command = args.command[1:] if args.command[:1] == ['--'] else args.command
34
+ if not command or not 1 <= args.seconds <= 600:
35
+ parser.error('provide a command and a wall limit of 1..600 seconds')
36
+ env = os.environ.copy()
37
+ env.update(CUDA_VISIBLE_DEVICES='', HIP_VISIBLE_DEVICES='', ROCR_VISIBLE_DEVICES='',
38
+ OMP_NUM_THREADS='2', MKL_NUM_THREADS='2', OPENBLAS_NUM_THREADS='2',
39
+ NUMEXPR_NUM_THREADS='2', HF_HUB_OFFLINE='1', TRANSFORMERS_OFFLINE='1',
40
+ PYTHONPATH=str(ROOT))
41
+ logs = ROOT / 'logs'
42
+ logs.mkdir(exist_ok=True)
43
+ with (logs / 'research.lock').open('w') as lock:
44
+ try:
45
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
46
+ except BlockingIOError:
47
+ parser.exit(2, 'Another foundry task is running.\n')
48
+ name = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S')
49
+ output = logs / f'{name}-{os.getpid()}.log'
50
+ start = time.monotonic()
51
+ timed_out = False
52
+ with output.open('wb') as transcript:
53
+ # GNU timeout owns a process group and kills descendants on expiry.
54
+ proc = subprocess.run(['timeout', '--signal=TERM', '--kill-after=5s',
55
+ f'{args.seconds}s', *command], cwd=args.cwd,
56
+ env=env, preexec_fn=constrain,
57
+ stdout=transcript, stderr=subprocess.STDOUT)
58
+ code = proc.returncode
59
+ timed_out = code in (124, 137)
60
+ result = dict(command=command, cwd=str(args.cwd.resolve()), exit_code=code,
61
+ timed_out=timed_out, elapsed_seconds=round(time.monotonic()-start, 3),
62
+ log=str(output), limits=dict(cpu_cores=2, address_space_gib=4,
63
+ wall_seconds=args.seconds, gpu=False))
64
+ output.with_suffix('.json').write_text(json.dumps(result, indent=2)+'\n')
65
+ print(json.dumps(result, indent=2))
66
+ print(''.join(output.read_text(errors='replace').splitlines(keepends=True)[-100:]))
67
+ return code
68
+
69
+
70
+ if __name__ == '__main__':
71
+ raise SystemExit(main())
72
+
73
+ #!/usr/bin/env python3
74
+ """Run a small Bunny request, retaining input, output and reported usage."""
75
+ import argparse
76
+ import datetime
77
+ import json
78
+ import os
79
+ from pathlib import Path
80
+ import subprocess
81
+
82
+ ROOT = Path(__file__).resolve().parents[1]
83
+
84
+
85
+ def main():
86
+ parser = argparse.ArgumentParser(description=__doc__)
87
+ parser.add_argument('prompt', type=Path)
88
+ args = parser.parse_args()
89
+ prompt = args.prompt.read_text()
90
+ if not prompt.strip() or len(prompt.encode()) > 16_384:
91
+ parser.error('prompt must be nonempty and at most 16 KiB')
92
+ wrapper = Path.home() / 'code/dotfiles/subagents/di-bunny.sh'
93
+ if not wrapper.is_file() or not os.environ.get('OPENROUTER_API_KEY'):
94
+ parser.exit(2, 'Bunny wrapper or OPENROUTER_API_KEY unavailable.\n')
95
+ tag = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S')
96
+ job = ROOT / 'logs' / f'bunny-{tag}-{os.getpid()}'
97
+ job.mkdir(parents=True)
98
+ (job / 'input.txt').write_text(prompt)
99
+ env = os.environ.copy()
100
+ env.update(DI_BUNNY_TIMEOUT_SECONDS='90', FX_MAX_AGENT_STEPS='6')
101
+ with (job / 'output.json').open('wb') as stdout, (job / 'stderr.log').open('wb') as stderr:
102
+ result = subprocess.run([str(wrapper), '--no-additional-dirs', 'ask', '--json',
103
+ '--no-save', '--no-color'], input=prompt.encode(),
104
+ stdout=stdout, stderr=stderr, env=env, cwd=ROOT)
105
+ usage = None
106
+ summary = ''
107
+ try:
108
+ data = json.loads((job / 'output.json').read_text())
109
+ usage = data.get('usage')
110
+ summary = data.get('final_output') or data.get('output') or ''
111
+ except (ValueError, AttributeError):
112
+ summary = 'No parseable response. Inspect stderr.log and output.json.'
113
+ (job / 'summary.txt').write_text(summary if isinstance(summary, str) else json.dumps(summary))
114
+ (job / 'metadata.json').write_text(json.dumps(dict(
115
+ exit_code=result.returncode, prompt_bytes=len(prompt.encode()), reported_usage=usage,
116
+ note='Usage is provider-reported, may be incomplete, and is not a hard token cap.',
117
+ timeout_seconds=90, max_agent_steps=6), indent=2)+'\n')
118
+ print(f'Bunny logs: {job}')
119
+ print(''.join((job / 'summary.txt').read_text().splitlines(keepends=True)[-100:]))
120
+ return result.returncode
121
+
122
+
123
+ if __name__ == '__main__':
124
+ raise SystemExit(main())
results/.gitkeep ADDED
File without changes
results/baselines.json ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "real_data_baselines",
3
+ "seed": 42,
4
+ "split": "60/20/20 stratified",
5
+ "metric": "roc_auc",
6
+ "source_sha256": "28a06cd9c7078da72f3dd951c8b05845a026a045071d015dcad5efb234c7bdd3",
7
+ "datasets": {
8
+ "adult": {
9
+ "rows": 48842,
10
+ "sha256": "cacdc745358d0b11edfb5625cc041a0cdb2c12550a55e52593abc32c64441d59",
11
+ "results": {
12
+ "prior": {
13
+ "val_auc": 0.5,
14
+ "test_auc": 0.5,
15
+ "fit_s": 0.01
16
+ },
17
+ "logreg": {
18
+ "val_auc": 0.907,
19
+ "test_auc": 0.9066,
20
+ "fit_s": 1.49
21
+ },
22
+ "rf": {
23
+ "val_auc": 0.9002,
24
+ "test_auc": 0.9034,
25
+ "fit_s": 3.16
26
+ },
27
+ "hgb": {
28
+ "val_auc": 0.9283,
29
+ "test_auc": 0.9272,
30
+ "fit_s": 2.24
31
+ },
32
+ "mlp": {
33
+ "val_auc": 0.9113,
34
+ "test_auc": 0.9083,
35
+ "fit_s": 8.27
36
+ }
37
+ },
38
+ "selected_by_val": "hgb"
39
+ },
40
+ "credit-g": {
41
+ "rows": 1000,
42
+ "sha256": "48b757fe48bd0b5c5ce2dbc84fabe20554bea43ce5b7157c6b5dbe8089107c0e",
43
+ "results": {
44
+ "prior": {
45
+ "val_auc": 0.5,
46
+ "test_auc": 0.5,
47
+ "fit_s": 0.0
48
+ },
49
+ "logreg": {
50
+ "val_auc": 0.8221,
51
+ "test_auc": 0.7942,
52
+ "fit_s": 0.09
53
+ },
54
+ "rf": {
55
+ "val_auc": 0.7961,
56
+ "test_auc": 0.7958,
57
+ "fit_s": 0.32
58
+ },
59
+ "hgb": {
60
+ "val_auc": 0.7806,
61
+ "test_auc": 0.7423,
62
+ "fit_s": 0.74
63
+ },
64
+ "mlp": {
65
+ "val_auc": 0.7986,
66
+ "test_auc": 0.7777,
67
+ "fit_s": 0.2
68
+ }
69
+ },
70
+ "selected_by_val": "logreg"
71
+ },
72
+ "bank-marketing": {
73
+ "rows": 45211,
74
+ "sha256": "2afe41dfb060eb46aeecb5c105085b1c9d5b0c522bba9185063d3a879d6fc5ba",
75
+ "results": {
76
+ "prior": {
77
+ "val_auc": 0.5,
78
+ "test_auc": 0.5,
79
+ "fit_s": 0.01
80
+ },
81
+ "logreg": {
82
+ "val_auc": 0.9047,
83
+ "test_auc": 0.9101,
84
+ "fit_s": 0.77
85
+ },
86
+ "rf": {
87
+ "val_auc": 0.9268,
88
+ "test_auc": 0.9267,
89
+ "fit_s": 2.71
90
+ },
91
+ "hgb": {
92
+ "val_auc": 0.9338,
93
+ "test_auc": 0.9375,
94
+ "fit_s": 1.52
95
+ },
96
+ "mlp": {
97
+ "val_auc": 0.9253,
98
+ "test_auc": 0.929,
99
+ "fit_s": 7.08
100
+ }
101
+ },
102
+ "selected_by_val": "hgb"
103
+ },
104
+ "phoneme": {
105
+ "rows": 5404,
106
+ "sha256": "986b259a8a108905122b32640c6241f2616fd3078c789c0e606930ebb48a5e40",
107
+ "results": {
108
+ "prior": {
109
+ "val_auc": 0.5,
110
+ "test_auc": 0.5,
111
+ "fit_s": 0.0
112
+ },
113
+ "logreg": {
114
+ "val_auc": 0.8013,
115
+ "test_auc": 0.8095,
116
+ "fit_s": 0.02
117
+ },
118
+ "rf": {
119
+ "val_auc": 0.9497,
120
+ "test_auc": 0.9554,
121
+ "fit_s": 0.51
122
+ },
123
+ "hgb": {
124
+ "val_auc": 0.9422,
125
+ "test_auc": 0.9489,
126
+ "fit_s": 0.71
127
+ },
128
+ "mlp": {
129
+ "val_auc": 0.8776,
130
+ "test_auc": 0.888,
131
+ "fit_s": 0.59
132
+ }
133
+ },
134
+ "selected_by_val": "rf"
135
+ },
136
+ "diabetes": {
137
+ "rows": 768,
138
+ "sha256": "4790da1f2df7137251f2076f8702565aa69046c26452728ed8f07cf44d466d9b",
139
+ "results": {
140
+ "prior": {
141
+ "val_auc": 0.5,
142
+ "test_auc": 0.5,
143
+ "fit_s": 0.0
144
+ },
145
+ "logreg": {
146
+ "val_auc": 0.8463,
147
+ "test_auc": 0.8031,
148
+ "fit_s": 0.05
149
+ },
150
+ "rf": {
151
+ "val_auc": 0.8084,
152
+ "test_auc": 0.7657,
153
+ "fit_s": 0.27
154
+ },
155
+ "hgb": {
156
+ "val_auc": 0.8126,
157
+ "test_auc": 0.7334,
158
+ "fit_s": 0.39
159
+ },
160
+ "mlp": {
161
+ "val_auc": 0.7233,
162
+ "test_auc": 0.7571,
163
+ "fit_s": 0.08
164
+ }
165
+ },
166
+ "selected_by_val": "logreg"
167
+ }
168
+ }
169
+ }
results/smoke.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "synthetic_smoke_only",
3
+ "seed": 42,
4
+ "device": "cpu",
5
+ "parameters": 577,
6
+ "train_rows": 384,
7
+ "validation_rows": 128,
8
+ "initial_loss": 0.6864046454429626,
9
+ "final_loss": 0.04011068865656853,
10
+ "validation_accuracy": 0.921875,
11
+ "elapsed_seconds": 1.5753,
12
+ "torch_version": "2.10.0+cpu",
13
+ "source_sha256": "8f21c7d1778a77dfa47b772b1488d8791df9b6b8a3e49ba6ae4429479c9a844e"
14
+ }
results/tuned-adult.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "tuned_ensemble",
3
+ "seed": 42,
4
+ "metric": "roc_auc",
5
+ "trials": 16,
6
+ "source_sha256": "43fb693e0248c9210f496bb4e592ab35756c835eb2db93d259efdc1b76200d08",
7
+ "datasets": {
8
+ "adult": {
9
+ "val_auc": 0.9296,
10
+ "test_auc": 0.929,
11
+ "ensemble": true,
12
+ "best_single_kind": "xgb",
13
+ "members": [
14
+ {
15
+ "kind": "xgb",
16
+ "params": {
17
+ "learning_rate": 0.057981034777209596,
18
+ "n_estimators": 584,
19
+ "max_depth": 4
20
+ }
21
+ },
22
+ {
23
+ "kind": "xgb",
24
+ "params": {
25
+ "learning_rate": 0.09491392939374098,
26
+ "n_estimators": 320,
27
+ "max_depth": 4
28
+ }
29
+ },
30
+ {
31
+ "kind": "lgbm",
32
+ "params": {
33
+ "learning_rate": 0.035153719464910554,
34
+ "n_estimators": 437,
35
+ "num_leaves": 32
36
+ }
37
+ }
38
+ ],
39
+ "artifact_bytes": 1017649,
40
+ "artifact_sha256": "c57ccbdbc6b9854c1118eaeccebfbe8d8f90444a7a678f3272d2fbcf81c19076",
41
+ "search_s": 206.2
42
+ }
43
+ }
44
+ }
results/tuned-bank-marketing.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "tuned_ensemble",
3
+ "seed": 42,
4
+ "metric": "roc_auc",
5
+ "trials": 16,
6
+ "source_sha256": "43fb693e0248c9210f496bb4e592ab35756c835eb2db93d259efdc1b76200d08",
7
+ "datasets": {
8
+ "bank-marketing": {
9
+ "val_auc": 0.9357,
10
+ "test_auc": 0.9386,
11
+ "ensemble": true,
12
+ "best_single_kind": "hgb",
13
+ "members": [
14
+ {
15
+ "kind": "hgb",
16
+ "params": {
17
+ "learning_rate": 0.03850363389390532,
18
+ "max_iter": 350,
19
+ "max_leaf_nodes": 28,
20
+ "min_samples_leaf": 15,
21
+ "l2_regularization": 0.37623611419432557,
22
+ "max_features": 0.911380806635415
23
+ }
24
+ },
25
+ {
26
+ "kind": "hgb",
27
+ "params": {
28
+ "learning_rate": 0.03770843066456067,
29
+ "max_iter": 563,
30
+ "max_leaf_nodes": 33,
31
+ "min_samples_leaf": 17,
32
+ "l2_regularization": 0.1653252347575385,
33
+ "max_features": 0.5319086280520877
34
+ }
35
+ },
36
+ {
37
+ "kind": "lgbm",
38
+ "params": {
39
+ "learning_rate": 0.035153719464910554,
40
+ "n_estimators": 437,
41
+ "num_leaves": 32
42
+ }
43
+ }
44
+ ],
45
+ "artifact_bytes": 2062973,
46
+ "artifact_sha256": "5a31f5eb6b4c6fc1f44d72ae0f804554bc62e35cf1ac7a979525001890503c18",
47
+ "search_s": 129.5
48
+ }
49
+ }
50
+ }
results/tuned-credit-g.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "tuned_ensemble",
3
+ "seed": 42,
4
+ "metric": "roc_auc",
5
+ "trials": 16,
6
+ "source_sha256": "43fb693e0248c9210f496bb4e592ab35756c835eb2db93d259efdc1b76200d08",
7
+ "datasets": {
8
+ "credit-g": {
9
+ "val_auc": 0.8325,
10
+ "test_auc": 0.7894,
11
+ "ensemble": true,
12
+ "best_single_kind": "logreg",
13
+ "members": [
14
+ {
15
+ "kind": "logreg",
16
+ "params": {
17
+ "C": 0.1
18
+ }
19
+ },
20
+ {
21
+ "kind": "logreg",
22
+ "params": {
23
+ "C": 1.0
24
+ }
25
+ },
26
+ {
27
+ "kind": "xgb",
28
+ "params": {
29
+ "learning_rate": 0.07046752877748709,
30
+ "n_estimators": 330,
31
+ "max_depth": 3
32
+ }
33
+ }
34
+ ],
35
+ "artifact_bytes": 75788,
36
+ "artifact_sha256": "64a01b461750a3bea41f4752977186dc5be4a0615d0c84b08b8e0b2751115e2f",
37
+ "search_s": 24.1
38
+ }
39
+ }
40
+ }
results/tuned-diabetes.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "tuned_ensemble",
3
+ "seed": 42,
4
+ "metric": "roc_auc",
5
+ "trials": 16,
6
+ "source_sha256": "43fb693e0248c9210f496bb4e592ab35756c835eb2db93d259efdc1b76200d08",
7
+ "datasets": {
8
+ "diabetes": {
9
+ "val_auc": 0.8528,
10
+ "test_auc": 0.8001,
11
+ "ensemble": true,
12
+ "best_single_kind": "logreg",
13
+ "members": [
14
+ {
15
+ "kind": "logreg",
16
+ "params": {
17
+ "C": 1.0
18
+ }
19
+ },
20
+ {
21
+ "kind": "logreg",
22
+ "params": {
23
+ "C": 0.1
24
+ }
25
+ },
26
+ {
27
+ "kind": "et",
28
+ "params": {}
29
+ }
30
+ ],
31
+ "artifact_bytes": 1085047,
32
+ "artifact_sha256": "50b5ecc4afc5e291dc45c7ce504a40fe2d5f735d3c86157a6c10a95407a9691f",
33
+ "search_s": 14.8
34
+ }
35
+ }
36
+ }
results/tuned-phoneme.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "tuned_ensemble",
3
+ "seed": 42,
4
+ "metric": "roc_auc",
5
+ "trials": 16,
6
+ "source_sha256": "43fb693e0248c9210f496bb4e592ab35756c835eb2db93d259efdc1b76200d08",
7
+ "datasets": {
8
+ "phoneme": {
9
+ "val_auc": 0.9523,
10
+ "test_auc": 0.9546,
11
+ "ensemble": false,
12
+ "best_single_kind": "et",
13
+ "members": [
14
+ {
15
+ "kind": "et",
16
+ "params": {}
17
+ }
18
+ ],
19
+ "artifact_bytes": 5496319,
20
+ "artifact_sha256": "e37ac3db2ba7bd9839833d8e0ac8a0a537467ddcf58b098d6550af47485fd683",
21
+ "search_s": 39.4
22
+ }
23
+ }
24
+ }
results/tuned.json ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "tuned_ensemble",
3
+ "seed": 42,
4
+ "metric": "roc_auc",
5
+ "trials": 16,
6
+ "source_sha256": "cf0c3d1191b0d15d1425aacb6e663f7e06abf721c2f01756cd653b2857dc0dd5",
7
+ "datasets": {
8
+ "adult": {
9
+ "val_auc": 0.929,
10
+ "test_auc": 0.9282,
11
+ "ensemble": true,
12
+ "members": [
13
+ {
14
+ "kind": "hgb",
15
+ "params": {
16
+ "learning_rate": 0.03850363389390532,
17
+ "max_iter": 350,
18
+ "max_leaf_nodes": 28,
19
+ "min_samples_leaf": 15,
20
+ "l2_regularization": 0.37623611419432557,
21
+ "max_features": 0.911380806635415
22
+ }
23
+ },
24
+ {
25
+ "kind": "hgb",
26
+ "params": {
27
+ "learning_rate": 0.03770843066456067,
28
+ "max_iter": 563,
29
+ "max_leaf_nodes": 33,
30
+ "min_samples_leaf": 17,
31
+ "l2_regularization": 0.1653252347575385,
32
+ "max_features": 0.5319086280520877
33
+ }
34
+ },
35
+ {
36
+ "kind": "hgb",
37
+ "params": {
38
+ "learning_rate": 0.026519966160905902,
39
+ "max_iter": 441,
40
+ "max_leaf_nodes": 58,
41
+ "min_samples_leaf": 25,
42
+ "l2_regularization": 0.07554804756926672,
43
+ "max_features": 0.5947356795421428
44
+ }
45
+ }
46
+ ],
47
+ "artifact_bytes": 2639714,
48
+ "artifact_sha256": "a28557fc9992cefa1eaaf2308964baa93b1c7e78f266c1a349fd64cdb1b246a7",
49
+ "search_s": 154.7
50
+ },
51
+ "credit-g": {
52
+ "val_auc": 0.8294,
53
+ "test_auc": 0.7986,
54
+ "ensemble": false,
55
+ "members": [
56
+ {
57
+ "kind": "logreg",
58
+ "params": {
59
+ "C": 0.1
60
+ }
61
+ }
62
+ ],
63
+ "artifact_bytes": 3369,
64
+ "artifact_sha256": "240366785f1e95fa3749b741747be6d4bb258d5be362e03cc5dec768b10d0496",
65
+ "search_s": 20.6
66
+ },
67
+ "bank-marketing": {
68
+ "val_auc": 0.9354,
69
+ "test_auc": 0.9384,
70
+ "ensemble": true,
71
+ "members": [
72
+ {
73
+ "kind": "hgb",
74
+ "params": {
75
+ "learning_rate": 0.03850363389390532,
76
+ "max_iter": 350,
77
+ "max_leaf_nodes": 28,
78
+ "min_samples_leaf": 15,
79
+ "l2_regularization": 0.37623611419432557,
80
+ "max_features": 0.911380806635415
81
+ }
82
+ },
83
+ {
84
+ "kind": "hgb",
85
+ "params": {
86
+ "learning_rate": 0.03770843066456067,
87
+ "max_iter": 563,
88
+ "max_leaf_nodes": 33,
89
+ "min_samples_leaf": 17,
90
+ "l2_regularization": 0.1653252347575385,
91
+ "max_features": 0.5319086280520877
92
+ }
93
+ },
94
+ {
95
+ "kind": "hgb",
96
+ "params": {
97
+ "learning_rate": 0.026519966160905902,
98
+ "max_iter": 441,
99
+ "max_leaf_nodes": 58,
100
+ "min_samples_leaf": 25,
101
+ "l2_regularization": 0.07554804756926672,
102
+ "max_features": 0.5947356795421428
103
+ }
104
+ }
105
+ ],
106
+ "artifact_bytes": 2670481,
107
+ "artifact_sha256": "ec72f430d51f216a15d3a611e6ff1ed279ecc022759160354cb821a6077f29ea",
108
+ "search_s": 101.4
109
+ },
110
+ "phoneme": {
111
+ "val_auc": 0.9523,
112
+ "test_auc": 0.9546,
113
+ "ensemble": false,
114
+ "members": [
115
+ {
116
+ "kind": "et",
117
+ "params": {}
118
+ }
119
+ ],
120
+ "artifact_bytes": 5496319,
121
+ "artifact_sha256": "e37ac3db2ba7bd9839833d8e0ac8a0a537467ddcf58b098d6550af47485fd683",
122
+ "search_s": 35.2
123
+ },
124
+ "diabetes": {
125
+ "val_auc": 0.8528,
126
+ "test_auc": 0.8001,
127
+ "ensemble": true,
128
+ "members": [
129
+ {
130
+ "kind": "logreg",
131
+ "params": {
132
+ "C": 1.0
133
+ }
134
+ },
135
+ {
136
+ "kind": "logreg",
137
+ "params": {
138
+ "C": 0.1
139
+ }
140
+ },
141
+ {
142
+ "kind": "et",
143
+ "params": {}
144
+ }
145
+ ],
146
+ "artifact_bytes": 1085047,
147
+ "artifact_sha256": "50b5ecc4afc5e291dc45c7ce504a40fe2d5f735d3c86157a6c10a95407a9691f",
148
+ "search_s": 16.5
149
+ }
150
+ }
151
+ }
src/.gitkeep ADDED
File without changes
tests/.gitkeep ADDED
File without changes
train/fetch_tabular.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, hashlib, sys
2
+ from pathlib import Path
3
+ from sklearn.datasets import fetch_openml
4
+ OUT = Path('/nvme0n1-disk/cache/foundry/tabular')
5
+ OUT.mkdir(parents=True, exist_ok=True)
6
+ SETS = {'adult': 1590, 'credit-g': 31, 'bank-marketing': 1461, 'phoneme': 1489, 'diabetes': 37}
7
+ meta = {}
8
+ for name, did in SETS.items():
9
+ b = fetch_openml(data_id=did, as_frame=True, parser='auto', data_home=str(OUT / 'openml'))
10
+ df = b.frame
11
+ p = OUT / f'{name}.parquet'
12
+ df.to_parquet(p)
13
+ meta[name] = dict(openml_id=did, rows=len(df), cols=df.shape[1], target=b.target.name,
14
+ sha256=hashlib.sha256(p.read_bytes()).hexdigest(), license=b.details.get('licence'),
15
+ version=b.details.get('version'))
16
+ print(name, meta[name], flush=True)
17
+ (OUT / 'manifest.json').write_text(json.dumps(meta, indent=2))
train/tabular_bench.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, hashlib, json, time
2
+ from pathlib import Path
3
+ import numpy as np, pandas as pd
4
+ from sklearn.compose import ColumnTransformer
5
+ from sklearn.dummy import DummyClassifier
6
+ from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
7
+ from sklearn.linear_model import LogisticRegression
8
+ from sklearn.metrics import roc_auc_score
9
+ from sklearn.model_selection import train_test_split
10
+ from sklearn.neural_network import MLPClassifier
11
+ from sklearn.pipeline import make_pipeline
12
+ from sklearn.preprocessing import OneHotEncoder, StandardScaler
13
+
14
+ DATA = Path('/nvme0n1-disk/cache/foundry/tabular')
15
+ SEED = 42
16
+
17
+
18
+ def load(name, target):
19
+ df = pd.read_parquet(DATA / f'{name}.parquet').dropna(subset=[target])
20
+ y = df[target]
21
+ y = (y == sorted(y.astype(str).unique())[-1]).astype(int).to_numpy() if y.nunique() == 2 else None
22
+ X = df.drop(columns=[target])
23
+ return X, y
24
+
25
+
26
+ def prep(X):
27
+ cat = [c for c in X.columns if X[c].dtype.name in ('category', 'object')]
28
+ num = [c for c in X.columns if c not in cat]
29
+ return ColumnTransformer([
30
+ ('n', make_pipeline(__import__('sklearn.impute', fromlist=['x']).SimpleImputer(strategy='median'), StandardScaler()), num),
31
+ ('c', make_pipeline(__import__('sklearn.impute', fromlist=['x']).SimpleImputer(strategy='most_frequent'),
32
+ OneHotEncoder(handle_unknown='ignore', min_frequency=5, sparse_output=False)), cat)])
33
+
34
+
35
+ def zoo(X):
36
+ return {
37
+ 'prior': lambda: DummyClassifier(strategy='prior'),
38
+ 'logreg': lambda: make_pipeline(prep(X), LogisticRegression(max_iter=300)),
39
+ 'rf': lambda: make_pipeline(prep(X), RandomForestClassifier(100, n_jobs=2, random_state=SEED)),
40
+ 'hgb': lambda: make_pipeline(prep(X), HistGradientBoostingClassifier(max_iter=200, learning_rate=0.1, random_state=SEED)),
41
+ 'mlp': lambda: make_pipeline(prep(X), MLPClassifier((128, 64), early_stopping=True, max_iter=60, random_state=SEED)),
42
+ }
43
+
44
+
45
+ def main():
46
+ ap = argparse.ArgumentParser()
47
+ ap.add_argument('--output', type=Path, required=True)
48
+ a = ap.parse_args()
49
+ manifest = json.loads((DATA / 'manifest.json').read_text())
50
+ out = dict(kind='real_data_baselines', seed=SEED, split='60/20/20 stratified', metric='roc_auc',
51
+ source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), datasets={})
52
+ for name, m in manifest.items():
53
+ X, y = load(name, m['target'])
54
+ if y is None:
55
+ continue
56
+ idx = np.arange(len(y))
57
+ tr, rest = train_test_split(idx, test_size=0.4, stratify=y, random_state=SEED)
58
+ va, te = train_test_split(rest, test_size=0.5, stratify=y[rest], random_state=SEED)
59
+ res = {}
60
+ for k, mk in zoo(X).items():
61
+ t = time.monotonic()
62
+ mdl = mk().fit(X.iloc[tr], y[tr])
63
+ fit = time.monotonic() - t
64
+ p = lambda i: mdl.predict_proba(X.iloc[i])[:, 1]
65
+ res[k] = dict(val_auc=round(roc_auc_score(y[va], p(va)), 4),
66
+ test_auc=round(roc_auc_score(y[te], p(te)), 4), fit_s=round(fit, 2))
67
+ print(name, k, res[k], flush=True)
68
+ best = max((k for k in res if k != 'prior'), key=lambda k: res[k]['val_auc'])
69
+ out['datasets'][name] = dict(rows=len(y), sha256=m['sha256'], results=res, selected_by_val=best)
70
+ a.output.parent.mkdir(parents=True, exist_ok=True)
71
+ a.output.write_text(json.dumps(out, indent=2) + '\n')
72
+
73
+
74
+ if __name__ == '__main__':
75
+ main()
train/tabular_tune.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, hashlib, json, time
2
+ from pathlib import Path
3
+ import joblib, numpy as np
4
+ from sklearn.ensemble import HistGradientBoostingClassifier, ExtraTreesClassifier
5
+ from sklearn.linear_model import LogisticRegression
6
+ from sklearn.metrics import roc_auc_score
7
+ from sklearn.model_selection import train_test_split
8
+ from sklearn.pipeline import make_pipeline
9
+ from tabular_bench import DATA, SEED, load, prep
10
+
11
+ TRIALS = 16
12
+ GB_TRIALS = 6
13
+
14
+
15
+ def gbdt(rng):
16
+ from xgboost import XGBClassifier
17
+ from lightgbm import LGBMClassifier
18
+ for _ in range(GB_TRIALS):
19
+ lr = float(10 ** rng.uniform(-2, -0.7)); n = int(rng.integers(150, 600)); d = int(rng.integers(3, 9))
20
+ yield 'xgb', dict(learning_rate=lr, n_estimators=n, max_depth=d), XGBClassifier(
21
+ learning_rate=lr, n_estimators=n, max_depth=d, subsample=0.8, colsample_bytree=0.8, n_jobs=2, random_state=SEED, verbosity=0)
22
+ yield 'lgbm', dict(learning_rate=lr, n_estimators=n, num_leaves=2 ** d), LGBMClassifier(
23
+ learning_rate=lr, n_estimators=n, num_leaves=2 ** d, subsample=0.8, subsample_freq=1, colsample_bytree=0.8, n_jobs=2, random_state=SEED, verbose=-1)
24
+
25
+
26
+ def hgb(rng):
27
+ return dict(learning_rate=float(10 ** rng.uniform(-2, -0.7)), max_iter=int(rng.integers(100, 600)),
28
+ max_leaf_nodes=int(rng.integers(8, 64)), min_samples_leaf=int(rng.integers(5, 60)),
29
+ l2_regularization=float(10 ** rng.uniform(-3, 1)), max_features=float(rng.uniform(0.5, 1.0)))
30
+
31
+
32
+ def main():
33
+ ap = argparse.ArgumentParser()
34
+ ap.add_argument('--output', type=Path, required=True)
35
+ ap.add_argument('--datasets', nargs='*')
36
+ ap.add_argument('--gbdt', action='store_true')
37
+ ap.add_argument('--artifacts', type=Path, required=True)
38
+ a = ap.parse_args()
39
+ man = json.loads((DATA / 'manifest.json').read_text())
40
+ out = dict(kind='tuned_ensemble', seed=SEED, metric='roc_auc', trials=TRIALS,
41
+ source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), datasets={})
42
+ a.artifacts.mkdir(parents=True, exist_ok=True)
43
+ for name, m in man.items():
44
+ if a.datasets and name not in a.datasets:
45
+ continue
46
+ X, y = load(name, m['target'])
47
+ if y is None:
48
+ continue
49
+ idx = np.arange(len(y))
50
+ tr, rest = train_test_split(idx, test_size=0.4, stratify=y, random_state=SEED)
51
+ va, te = train_test_split(rest, test_size=0.5, stratify=y[rest], random_state=SEED)
52
+ rng = np.random.default_rng(SEED)
53
+ t0 = time.monotonic()
54
+ cands = []
55
+ for i in range(TRIALS):
56
+ p = hgb(rng)
57
+ mdl = make_pipeline(prep(X), HistGradientBoostingClassifier(early_stopping=False, random_state=SEED, **p)).fit(X.iloc[tr], y[tr])
58
+ cands.append(('hgb', p, mdl, mdl.predict_proba(X.iloc[va])[:, 1]))
59
+ for c in (0.1, 1.0):
60
+ mdl = make_pipeline(prep(X), LogisticRegression(C=c, max_iter=500)).fit(X.iloc[tr], y[tr])
61
+ cands.append(('logreg', dict(C=c), mdl, mdl.predict_proba(X.iloc[va])[:, 1]))
62
+ mdl = make_pipeline(prep(X), ExtraTreesClassifier(300, min_samples_leaf=2, n_jobs=2, random_state=SEED)).fit(X.iloc[tr], y[tr])
63
+ cands.append(('et', {}, mdl, mdl.predict_proba(X.iloc[va])[:, 1]))
64
+ if a.gbdt:
65
+ for kind, p, est in gbdt(np.random.default_rng(SEED + 1)):
66
+ mdl = make_pipeline(prep(X), est).fit(X.iloc[tr], y[tr])
67
+ cands.append((kind, p, mdl, mdl.predict_proba(X.iloc[va])[:, 1]))
68
+ aucs = [roc_auc_score(y[va], c[3]) for c in cands]
69
+ order = np.argsort(aucs)[::-1]
70
+ top = [cands[i] for i in order[:3]]
71
+ ens_va = np.mean([c[3] for c in top], axis=0)
72
+ ens_auc = roc_auc_score(y[va], ens_va)
73
+ single = cands[order[0]]
74
+ use_ens = ens_auc > aucs[order[0]]
75
+ members = top if use_ens else [single]
76
+ pred = lambda i: np.mean([c[2].predict_proba(X.iloc[i])[:, 1] for c in members], axis=0)
77
+ path = a.artifacts / f'{name}.joblib'
78
+ joblib.dump(dict(members=[c[2] for c in members], columns=list(X.columns)), path, compress=3)
79
+ out['datasets'][name] = dict(
80
+ val_auc=round(max(ens_auc, aucs[order[0]]), 4), test_auc=round(roc_auc_score(y[te], pred(te)), 4),
81
+ ensemble=use_ens, best_single_kind=single[0], members=[dict(kind=c[0], params=c[1]) for c in members],
82
+ artifact_bytes=path.stat().st_size, artifact_sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
83
+ search_s=round(time.monotonic() - t0, 1))
84
+ print(name, out['datasets'][name], flush=True)
85
+ a.output.write_text(json.dumps(out, indent=2) + '\n')
86
+
87
+
88
+ if __name__ == '__main__':
89
+ main()