Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
**/*__pycache__
**/*.pem
**/*.sqlite3
hudes_env/
hudes/mnist_data/
web/mnist_data/
mnist_data/
web/node_modules/
web/dist/
web/test-results/
web/generated/
web/bundle-visualization.html
web/package-lock.json
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ repos:
- id: end-of-file-fixer
- id: check-ast
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3
hooks:
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## Featured videos

- [Human Descent speed run — October 20, 2025](https://youtu.be/PIFJnmvqMIk)
- [3D speed run — November 18, 2025](https://www.youtube.com/watch?v=4exJirHbESM)
- [1D speed run — November 18, 2025](https://www.youtube.com/watch?v=t4DESt8-PwQ)
- [Human Descent — How to play](https://youtu.be/tspa15Ei3KI)

**Live server:** The web version runs at [humandescent.net](https://humandescent.net) on an RTX 4090 GPU. Expect instability if many people connect simultaneously.
Expand All @@ -14,6 +15,11 @@ Human Descent opens the door for you to explore neural network optimization in h
### Older demo
[Demo video](https://youtu.be/mqAmaBP3-Q4)

## Game modes

- **3D landscape (default):** renders three coupled loss surfaces that update in real time while you explore six random directions.
- **1D slices (experimental):** renders six fast-updating loss curves, one per dimension. Launch with `?mode=1d` (e.g. `http://localhost:5173/?mode=1d`) or use the View toggle in the header to switch between modes.

## Batch-parameterized PyTorch layers

The visualizations in this project rely on a custom set of layers found in `hudes/model_first/model_first_nn.py`. These helpers wrap familiar PyTorch modules (linear, convolution, pooling, flatten, sequential) so that every forward pass accepts not just a minibatch of data, but also a *batch of parameter tensors*. Key pieces include:
Expand Down Expand Up @@ -50,3 +56,15 @@ Iteratively selecting between new training batches and different random subspace
For example its possible to train a 26,000 parameter MNIST model using a 6 dimensional keyboard input in about 10 minutes.

![Example snapshot](images/01_demo.jpg)

## Testing

- Run all frontend checks with `cd web && npm test` (wrap in the repo's virtualenv so the backend can start).
- When iterating on the speed-run or highscores flows, capture deterministic Playwright logs with:

```
cd web
(npm run -s test:e2e:chromium -- tests/speedrun.spec.mjs) 2>&1 3>&1 | tee /tmp/play-speedrun.log >/dev/null
```

That command mirrors what we use in CI so any browser/server output is preserved under `/tmp/play-speedrun.log` for later debugging.
36 changes: 36 additions & 0 deletions cleanup_5s_runs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os
import sqlite3
import sys

# Add current directory to sys.path so we can import hudes
sys.path.append(os.getcwd())

from hudes.high_scores import DB_DEFAULT_PATH


def cleanup():
path = os.environ.get("HIGH_SCORES_PATH", DB_DEFAULT_PATH)
print(f"Opening DB at {path}")

if not os.path.exists(path):
print("Database file not found!")
return

try:
with sqlite3.connect(path) as conn:
cur = conn.execute("SELECT COUNT(*) FROM high_scores WHERE duration = 5")
count = cur.fetchone()[0]
print(f"Found {count} runs with duration 5s")

if count > 0:
conn.execute("DELETE FROM high_scores WHERE duration = 5")
conn.commit()
print(f"Successfully deleted {count} records.")
else:
print("No records found to delete.")
except Exception as e:
print(f"Error: {e}")


if __name__ == "__main__":
cleanup()
392 changes: 146 additions & 246 deletions design.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion hudes/high_scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,10 @@ def get_rank(score: float, path: str | None = None) -> tuple[int, int]:
(float(score),),
)
less = int(cur.fetchone()[0])
return less + 1 if total > 0 else 0, total
return less + 1, total


def delete_high_score(row_id: int, path: str | None = None) -> bool:
with _conn(path) as conn:
cur = conn.execute("DELETE FROM high_scores WHERE id=?", (row_id,))
return cur.rowcount > 0
28 changes: 28 additions & 0 deletions hudes/hudes.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ message Config {
required int32 mesh_grids = 5;
required int32 batch_size = 6;
required string dtype = 7;
optional bool mesh_enabled = 8 [default = true];
optional int32 loss_lines = 9;
optional bool resume_supported = 10 [default = true];
optional string client_session_token = 11;
}

message TrainLossAndPreds {
Expand All @@ -26,6 +30,8 @@ message TrainLossAndPreds {
}
message ValLoss {
required double val_loss = 1;
optional int32 rank = 2;
optional int32 total_scores = 3;
}

message BatchExamples {
Expand All @@ -51,6 +57,7 @@ message Control {
CONTROL_BATCH_EXAMPLES = 3;
CONTROL_SPEED_RUN_START = 10;
CONTROL_HIGH_SCORE_LOG = 11;
CONTROL_SPEED_RUN_CANCEL = 12;
// Request the top 100 leaderboard entries (NAME/SCORE only) over WebSocket
CONTROL_LEADERBOARD_REQUEST = 106;
// Response containing the leaderboard arrays (parallel arrays)
Expand All @@ -60,6 +67,8 @@ message Control {
CONTROL_CONFIG = 103;
CONTROL_MESHGRID_RESULTS= 104;
CONTROL_SGD_STEP = 105;
CONTROL_LOSS_LINE_RESULTS = 108;
CONTROL_RESUME = 109;
CONTROL_QUIT = 901;
}
required Type type = 1;
Expand All @@ -82,6 +91,7 @@ message Control {
optional int32 speed_run_seconds_remaining = 212;
// True when a VAL message marks the end of the Speed Run window
optional bool speed_run_finished = 213;
optional bool speed_run_active = 221;
// Top leaderboard (NAME/SCORE only). Parallel arrays; same length.
// Only set in CONTROL_LEADERBOARD_RESPONSE.
repeated string leaderboard_names = 214;
Expand All @@ -94,5 +104,23 @@ message Control {
optional int32 request_idx = 4;
}
optional HighScore high_score = 211; // used with CONTROL_HIGH_SCORE_LOG
repeated float loss_line_results = 216;
repeated int32 loss_line_shape = 217;
message Resume {
enum Status {
RESUME_OK = 0;
RESUME_NOT_FOUND = 1;
RESUME_EXPIRED = 2;
RESUME_REJECTED = 3;
}
optional Status status = 1;
optional string token = 2;
optional int32 last_request_idx = 3;
optional string client_session_token = 4;
optional string new_client_session_token = 5;
}
optional Resume resume = 218;
optional string resume_token = 219;
optional int32 total_eval_steps = 220;

}
40 changes: 22 additions & 18 deletions hudes/hudes_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion hudes/hudes_play.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def main(argv=None):
"--skip-help",
action="store_true",
)
parser.add_argument(
"--headless",
action="store_true",
help="Use non-OpenGL view when running keyboardGL (useful for CI/headless)",
)
parser.add_argument("--max-loop-iterations", type=int, default=None)
parser.add_argument("--max-seconds", type=float, default=None)

Expand Down Expand Up @@ -52,7 +57,10 @@ def main(argv=None):
joystick_controller_key=args.controller,
skip_help=args.skip_help,
)
view = OpenGLView(grid_size=args.grid_size, grids=args.grids)
if args.headless:
view = View()
else:
view = OpenGLView(grid_size=args.grid_size, grids=args.grids)
elif args.input == "xtouch":
controller = XTouchClient(
addr=args.addr,
Expand Down
Loading
Loading