Skip to content

Run the clinician demo and inference on Apple silicon and CPU - #277

Merged
amrit110 merged 11 commits into
mainfrom
demo-apple-silicon
Oct 4, 2026
Merged

amrit110 merged 11 commits into
mainfrom
demo-apple-silicon

Conversation

@amrit110

@amrit110 amrit110 commented Oct 4, 2026

Copy link
Copy Markdown
Member

Stacked on #250 (clinician-demo). Lets a trained hybrid checkpoint run without CUDA, so the clinician demo can run entirely on a laptop (tested on an Apple M4).

What changes

  • odyssey/models/backbones/mamba_portable.py (new). Pure-PyTorch Mamba-2 (with our carried-state fix), MHA and RMSNorm. Parameter names match mamba-ssm 2.3.0, so a GPU checkpoint loads unchanged. Inference only: no backward kernel, no single-token decode, no varlen.
  • EHRHybridBackbone picks its layers in two small helpers (_mixer_classes, _inference_params_cls): the CUDA kernels when mamba-ssm imports, otherwise the portable layers. The CUDA path is unchanged.
  • odyssey/utils/device.py (new). One place for device choice (cuda, then mps, then cpu) and transfer (MPS has no float64; time stamps become float32, which the model casts to anyway).
  • One chunk-move helper. streaming.move_to_device existed next to a copy, train._move_chunk_to_device; the copy is removed and its callers use move_to_device.
  • Demo: --device auto (default). New python -m apps.clinician_demo.export_thresholds --run-dir … writes the alert lines as aggregates (demo_thresholds_aggregate.json). The demo reads that file when the patient-level alerts_rows.parquet is absent. Runbook section added in docs/clinician_demo.md.
  • Steering and concept attribution keep CUDA-else-CPU: they accumulate in float64 on the device.

Evidence

  • GPU vs port, real model (full_run_v10, 12 open-demo patients, ~165k positions; CUDA run on the A100):

    mean abs diff p99 max
    risk (MPS vs CUDA) 0.0002 0.002 0.009
    concept beliefs 0.006
    top-1 next event agreement 99.8%

    CPU and MPS agree with each other more closely (max 0.002) than either agrees with CUDA. The gap matches TF32 / Triton accumulation order, not a port error.

  • Speed on an M4 (MPS): 2 min for the 12 patients (CPU: 5.5 min). In the demo a 6,000-entry stay traces in about 10 s.

  • Browser check (open mode only): gallery, replay, risk tiles and chart, concept beliefs, what-if (systolic 80 raises 24 h death risk 1.3% to 1.8%), evidence (40 re-runs in about 30 s) and scorecard all work, with no console errors. Also checked with run A (full_run_v10_summary2), which needs Add a self-supervised window-summary head and the window-statistic probes #251's code to load.

Tests

  • New test_mamba_portable.py (17): chunked scan vs the step-by-step SSM recurrence (ragged lengths, grouped B/C, with and without an initial state), chunk carry-over, chunk-size invariance, norm and causal/GQA attention formulas, parameter names pinned to mamba-ssm, unsupported paths raise.
  • New test_hybrid_portable.py: the backbone builds and streams with carried state on CPU; MPS matches CPU.
  • New test_mamba_portable_gpu.py: the whole backbone, one state dict, CUDA kernels vs portable (on CUDA and on CPU). Passed on the A100, together with the existing test_hybrid_gpu.py / test_mamba2_patch_gpu.py.
  • New test_device.py, plus alert-line export/load/fallback and --device auto tests in the demo suite.
  • The old test asserting that the hybrid backbone raises without CUDA is removed; that behaviour is now the feature.
  • Local: 1944 passed, 10 skipped. Ruff, ruff format and mypy are clean.
  • Already failing before this change: test_train_gpu.py::test_train_writes_provenance_for_periodic_and_epoch_checkpoints and ::test_train_resumes_mid_epoch_by_fast_forwarding fail on unmodified clinician-demo code too (A100, same assertions).

Data handling

Only open-mode data goes to a laptop. Patient-level files (alerts_rows.parquet, credentialed shards, the full split file) stay on the GPU host; the runbook lists exactly what to copy.

🤖 Generated with Claude Code

amrit110 and others added 10 commits September 11, 2026 07:32
Three readers streamed one patient through the model with their own
copy of the same PackedLaneSampler loop. It now lives once in
odyssey/inference/patient_stream.py (stream_patient, risk_within), and
case_study and counterfactual use it. Behaviour is unchanged: the
existing case-study and counterfactual tests pass unmodified.

Also added, each with tests:
- extract_patient_case reports risk at 8/24/72 h (event_risk_by_horizon);
  event_risk_24h is kept for the report that reads it.
- ordered_sequence_rows: the exact row order build_patient_sequence
  tokenizes, so a position maps back to its raw row.
- load_meds_subject: one subject from one shard, filter pushed into the scan.
- code_metadata.load_code_descriptions, split out of steering so readers
  can name codes without importing steering.
- occlude_codes: target-agnostic occlusion with a progress callback;
  occlusion_attribution is now a thin wrapper, plus
  event_occlusion_attribution for an event's risk.
- concept_display_name moves from make_readout_table into concepts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zuuVqnBfTsozfaj5F4y3r
A web app that replays one admission and shows, moment by moment, the
model's risk of ICU admission, vasopressors, AKI, Sepsis-3 and death
against an alert line, what it thinks is going on, what it expects
next, what-if value edits, occlusion evidence, and a scorecard against
the tuned GBM. Runbook: docs/clinician_demo.md.

- Standard library HTTP only (the GPU host's environment is pinned);
  loopback bind, Host allowlist, custom API header, no CORS, no-store,
  same-origin CSP, static path containment.
- Two data modes: credentialed (held-out MIMIC-IV) and open (MIMIC-IV
  Clinical Database Demo). Every chart says whether the model trained
  on that patient.
- Honest by construction: the gallery shows misses and false alarms with
  their rates; lead time counts from the alert episode still on at the
  event; risk is hidden after onset; readmission, steering, label
  overrides and rollouts are never shown (a test enforces the imports).
- Vanilla ES modules, no build step, no third-party JS.
- 200+ CPU tests on synthetic data: every module, server security over
  a real socket, and spies proving the run's chunk_size reaches every
  model call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zuuVqnBfTsozfaj5F4y3r
- server: static files are served from a map built once at startup
  (static_files); a request is a dictionary lookup, so no filesystem
  path is ever built from request data. Clears the three CodeQL
  "uncontrolled data used in path expression" alerts. Symlinks out of
  the root and non-allowlisted extensions are still excluded, now at
  map-build time; tests cover both.
- showcase: rename the `_thr` column and `thr` test argument, which the
  typos hook reads as "the".
- pre-commit: no hook rewrites scripts/gemini/out/ (pre-commit.ci's
  end-of-file fixer had edited 12 verbatim GEMINI exports on this PR),
  and mypy/typos skip the two cohort producers committed verbatim as
  they ran. pyproject's mypy exclude never applied to them because
  pre-commit passes files by name; this is why main's code check has
  failed since 0de877f.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zuuVqnBfTsozfaj5F4y3r
This reverts commit 8348226, which
added a trailing newline to 12 files under scripts/gemini/out/evals/.
Those files are GEMINI's exported reports, kept byte-for-byte; the
previous commit stops the hooks from touching them again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zuuVqnBfTsozfaj5F4y3r
Front end only; the API and Python are unchanged.

- Light theme by default, dark via a footer toggle. One accent colour;
  event colours only on lines and dots.
- Replay: patient one-liner, sticky "now" bar (play, scrub, clock time),
  five risk tiles with a status word (Low / Watch / Alert on / Happened)
  and "n× the average patient", then the chart, what happened in the
  stay, what the model believes now, and the what-if / evidence tools.
- The chart fades everything after "now".
- Alert stories are composed client-side in clock time ("Began Day 1,
  22:53. The alert had been on since Day 1, 01:16: 22 h of warning").
- The 29-row concept strip, alert-line statistics and next-token list
  sit behind disclosures; model name and checkpoint move to the footer.
- Gallery cards carry one key line; the full admission list is a
  collapsed per-patient list with a note on how many were in training.
- Scorecard cells show one number per model, intervals on hover.
- Fix: class display rules overrode the browser's [hidden] rule, so the
  search box showed in open mode and "Alert on now" showed on every row.
- Runbook: static files can be copied over a running deployment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QebToqtFMVakRkx3PhF3ZX
The hybrid backbone imported mamba-ssm unconditionally, so a trained model
could only run on a CUDA host. Without mamba-ssm it now builds from
odyssey/models/backbones/mamba_portable.py: Mamba-2 (with the carried-state
fix), MHA and RMSNorm in plain PyTorch, with mamba-ssm's parameter names,
so a GPU checkpoint loads unchanged. Inference only.

- odyssey/utils/device.py: one place for device choice (cuda, then mps,
  then cpu) and transfer (float64 time stamps become float32 on MPS).
- streaming.move_to_device is now the single chunk-move helper; the
  duplicate train._move_chunk_to_device is gone.
- The demo takes --device auto (default) and can read its alert lines from
  an exported aggregate file (apps.clinician_demo.export_thresholds) when
  the patient-level alerts_rows.parquet is not on the host.
- Steering and concept attribution keep CUDA-else-CPU: they accumulate in
  float64 on device.

Checked against the A100 on 12 open-demo patients (full_run_v10, ~165k
positions): mean risk difference 0.0002, max 0.009; top-1 next-event
agreement 99.8%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.pre-commit-config.yaml
#	_typos.toml
…silicon

# Conflicts:
#	odyssey/inference/run_inference.py
docs/checkpoints.md: what a run directory holds, how full_run_v10 was
trained (one joint run, no separate pretraining or fine-tuning), how to
prepare MEDS data with the checkpoint's own vocabulary and bins, how to
score a patient (the example was run on full_run_v10), the portable
backend, and the data-governance terms. It also says that EHR-Mamba3,
ehr_mamba3.yaml and scripts/meds/run_pipeline.sh are from an older
version of the repository. The README points to it.

Also: the alert-line constant's docstring became a comment
(check-docstring-first read it as a second module docstring), and
scripts/probe_channel.py, new on main, now uses move_to_device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@amrit110

amrit110 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

Update:

  • Merged the updated clinician-demo (now containing main). Resolved one import conflict in run_inference.py and moved scripts/probe_channel.py (new on main) to move_to_device.
  • Fixed pre-commit.ci: the constant's docstring in thresholds.py is now a comment (check-docstring-first).
  • Added docs/checkpoints.md: how to use a trained checkpoint (contents, how full_run_v10 was trained, data prep with the checkpoint's own vocabulary and bins, a per-patient scoring example run on full_run_v10, data-governance terms, and a note that EHR-Mamba3, ehr_mamba3.yaml and scripts/meds/run_pipeline.sh are from an older version). The README links to it.
  • Local: 1997 passed, 10 skipped. Once Add a clinician demo app for the MIMIC-IV forecasting model #250 merges, this PR's base becomes main and the full CI runs.

🤖 Generated with Claude Code

Base automatically changed from clinician-demo to main October 4, 2026 05:54
# Conflicts:
#	apps/clinician_demo/__main__.py
#	apps/clinician_demo/service.py
#	apps/clinician_demo/thresholds.py
#	docs/clinician_demo.md
#	odyssey/inference/counterfactual.py
#	odyssey/inference/patient_stream.py
#	tests/apps/clinician_demo/test_main_and_layering.py
#	tests/apps/clinician_demo/test_thresholds.py
@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.41667% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.98%. Comparing base (56f4345) to head (3a7266e).

Files with missing lines Patch % Lines
odyssey/models/backbones/hybrid.py 62.50% 6 Missing ⚠️
odyssey/inference/counterfactual.py 50.00% 1 Missing ⚠️
odyssey/inference/leakage.py 50.00% 1 Missing ⚠️
odyssey/inference/run_inference.py 75.00% 1 Missing ⚠️
odyssey/models/backbones/mamba_portable.py 99.44% 1 Missing ⚠️
odyssey/utils/device.py 92.85% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #277      +/-   ##
==========================================
+ Coverage   90.51%   90.98%   +0.47%     
==========================================
  Files          69       71       +2     
  Lines       11073    11263     +190     
==========================================
+ Hits        10023    10248     +225     
+ Misses       1050     1015      -35     
Files with missing lines Coverage Δ
odyssey/data/streaming.py 100.00% <100.00%> (ø)
odyssey/inference/alerts.py 87.64% <100.00%> (-0.02%) ⬇️
odyssey/inference/concept_attribution.py 85.02% <100.00%> (-0.08%) ⬇️
odyssey/inference/embedding_probe.py 100.00% <100.00%> (ø)
odyssey/inference/interventions.py 94.20% <100.00%> (-0.02%) ⬇️
odyssey/inference/patient_stream.py 100.00% <100.00%> (ø)
odyssey/inference/rollouts.py 97.43% <100.00%> (-0.02%) ⬇️
odyssey/inference/time_head_probe.py 80.33% <100.00%> (ø)
odyssey/training/train.py 88.71% <100.00%> (-0.13%) ⬇️
odyssey/utils/env_fingerprint.py 100.00% <100.00%> (ø)
... and 6 more

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@amrit110
amrit110 merged commit 15fb2e9 into main Oct 4, 2026
10 checks passed
@amrit110
amrit110 deleted the demo-apple-silicon branch October 4, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant