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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
SENTRY_AUTH_TOKEN=

# Sentry DSN — used by Sentry.jl SDK for error capture
# Find in: https://limen-neural.sentry.io/projects/liquidcortex/settings/keys/
# MUST be the DSN for project **liquidcortex**, not rust/python.
# Client keys: https://limen-neural.sentry.io/settings/projects/liquidcortex/keys/
# The project id is the last path segment of the DSN (…/ingest…/PROJECT_ID).
# liquidcortex → 4511697978982400 | rust → 4511355448066048 (do not use for LiquidCortex)
SENTRY_DSN=

# Sentry org and project (defaults match CI workflow values)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
julia-version: ['1.10', '1.11', '1.12']
julia-version: ['1.12']
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
rmems marked this conversation as resolved.

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
julia-version: ['1.11']
julia-version: ['1.12']

steps:
- name: Checkout
Expand Down
86 changes: 86 additions & 0 deletions .github/workflows/gpu-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: GPU CI (self-hosted)

# Runs Julia package tests on the local RTX-class runner (labels: self-hosted, Linux, X64, gpu).
# CPU smoke stays on ubuntu-latest in ci.yml; this job exercises CUDA kernels.
#
# Security: never run untrusted fork PR code on the persistent self-hosted host
# (GitHub recommendation). Same-repo PRs, pushes, and manual dispatch only.
on:
pull_request:
branches:
- '**'
Comment thread
rmems marked this conversation as resolved.
push:
branches:
- main
- 'release/*'
workflow_dispatch:
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Serialize per-ref so an unrelated PR does not cancel main/release GPU jobs.
# The single self-hosted runner still runs at most one job at a time.
concurrency:
group: liquidcortex-gpu-${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
gpu-test:
name: Julia 1.12 — GPU tests (self-hosted)
# Fork PRs: skip (do not checkout untrusted code onto the GPU host).
if: >
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: [self-hosted, Linux, X64, gpu]
Comment thread
rmems marked this conversation as resolved.
timeout-minutes: 60

env:
# Optional: report step! failures to liquidcortex project during CI
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
JULIA_NUM_THREADS: "1"
JULIA_PKG_PRECOMPILE_AUTO: "1"

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Verify host toolchain
run: |
set -euo pipefail
command -v julia
julia --version
Comment thread
rmems marked this conversation as resolved.
# Job claims Julia 1.12; fail early if runner default drifts.
julia -e 'VERSION.major == 1 && VERSION.minor == 12 || error("expected Julia 1.12, got $VERSION")'
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader

- name: Instantiate
run: julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()'
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Verify CUDA.jl in project
run: |
set -euo pipefail
julia --project=. -e '
using CUDA
@assert CUDA.functional() "CUDA.jl reports non-functional device"
println(CUDA.name(CUDA.device()))
'

- name: Run tests (CPU + GPU)
run: julia --project=. -e 'using Pkg; Pkg.test()'

- name: Reclaim GPU memory
if: always()
run: |
julia --project=. -e '
using CUDA
if CUDA.functional()
CUDA.synchronize()
GC.gc(true)
CUDA.reclaim()
free = CUDA.free_memory() / 1e9
total = CUDA.total_memory() / 1e9
println("VRAM free/total GB: ", round(free; digits=2), " / ", round(total; digits=2))
end
'
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ Manifest.toml
# Local environment variables (contains secrets — never commit)
.env
.mimocode/

# Local exploration / agent notes (never ship)
.explore-notes/
playground-results/
.cursor/
18 changes: 16 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,38 @@ Julia package with CUDA acceleration, cuSPARSE Float16, STDP covariance learning
- Julia standard formatting
- No domain-specific code in core (market/mining removed in PR #12)
- Generic inhibition interface: `step!(brain::SparseBrain, u::CuVector{Float32}; inhibition::Real=0.0, reflex_eta::Real=ETA)`
- Step kwargs: `plasticity=:readout_only` (default), `:recurrent_stdp`, `:none`; plus `sync`, `record_history`, `use_device_noise`, `recurrent_eta`
- Configurable dimensions: `SparseBrain(tau_m::Float32; n_in::Int=14, n_out::Int=16, name::String="default")`
- Compat: CUDA.jl `6` (latest); local TDD/verify on Julia 1.12

## Testing

- CPU tests always run (package load, API exports, config validation)
- GPU tests gated by `LiquidCortex._cuda_available[]`
- All 3 Julia versions tested: 1.10, 1.11, 1.12
- CI workflows run Julia **1.12** only (compat still declares 1.10–1.12)
Comment thread
rmems marked this conversation as resolved.
- **CPU smoke:** `.github/workflows/ci.yml` → `ubuntu-latest`
- **GPU tests:** `.github/workflows/gpu-ci.yml` → self-hosted runner labels
`self-hosted`, `Linux`, `X64`, `gpu` (local RTX host under
`~/actions-runner/LiquidCortex.jl-runner/`, not inside the git clone)
- GPU jobs use a repo-wide concurrency group so only one GPU suite runs at a time

## PR Instructions

- Branch naming: `feature/`, `fix/`, `ci/`, `refactor/`, `docs/`
- Run tests before pushing
- All CI checks must pass (Julia 1.10/1.11/1.12, Codacy, CodeRabbit)
- All CI checks must pass (Julia 1.12, Codacy, CodeRabbit)
- Address all bot review threads before merge
- Pin GitHub Actions to full commit SHAs (not tags)
- Use `julia-actions/julia-processcoverage` for coverage — not Coverage.jl in Project.toml
- README must use pure markdown — no HTML elements (Codacy lints `<a>` and `<img>`)

## Sentry

- Runtime capture uses `ENV["SENTRY_DSN"]` (see `.env.example`).
- DSN **must** target project **`liquidcortex`** (`SENTRY_ORG=limen-neural`, `SENTRY_PROJECT=liquidcortex`).
- Do **not** reuse the **rust** project DSN — events will misroute (issue IDs like `RUST-*` with `package=LiquidCortex.jl`).
- Quick check: DSN path suffix for liquidcortex is `…/4511697978982400` (rust ends in `…/4511355448066048`).

## Cursor Cloud specific instructions

- Julia is provided via `juliaup` with default channel **1.12** (within this repo's `1.10, 1.11, 1.12` compat). Standard setup applies: `julia --project -e 'using Pkg; Pkg.instantiate()'` then `julia --project -e 'using Pkg; Pkg.test()'`.
Expand Down
12 changes: 6 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "LiquidCortex"
uuid = "1f75a0df-502a-4be0-9310-97a9e2e7f166"
version = "0.2.0"
license = "MIT OR Apache-2.0"
version = "0.2.0"

[deps]
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
Expand All @@ -12,13 +12,13 @@ Sentry = "22473447-a6cd-4572-bdc6-902d1f188b6a"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[compat]
CUDA = "6"
Sentry = "0.2"
julia = "1.10, 1.11, 1.12"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

[compat]
CUDA = "5"
Sentry = "0.2"
julia = "1.10, 1.11, 1.12"
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,31 @@ output = get_output(brain)
|-----------------|-------------|
| `SparseBrain(tau_m; n_in, n_out)` | Create a 65,536-neuron sparse reservoir lobe |
| `EnsembleBrain(; n_in, n_out)` | Create 4-lobe ensemble (262,144 neurons) |
| `step!(brain, u; inhibition, reflex_eta)` | Execute one simulation timestep |
| `ensemble_step!(eb, u; inhibition, reflex_eta, reflex_signal)` | Step all lobes and aggregate |
| `step!(brain, u; inhibition, reflex_eta, ...)` | Execute one simulation timestep (see experimental kwargs) |
| `ensemble_step!(eb, u; inhibition, reflex_eta, reflex_signal, ...)` | Step all lobes and aggregate |
| `get_output(brain)` | Copy readout from GPU to CPU |
| `get_ensemble_output(eb)` | Copy aggregated readout |
| `compute_reservoir_covariance!(brain)` | Compute subsampled covariance matrix |
| `diagnostics(brain)` | Return diagnostic string |
| `ensemble_diagnostics(eb)` | Per-lobe diagnostic summary |

## Experimental step API

LiquidCortex is an experimental Julia package. Defaults are intentional:

| Keyword | Default / values | Meaning |
|---------|------------------|---------|
| `plasticity` | **default** `:readout_only` | Frozen recurrent `W`; Hebbian `W_out` every 10 ticks |
| | opt-in `:recurrent_stdp` | Experimental pair STDP on sparse edges every tick |
| | opt-in `:none` | No weight updates |
| `recurrent_eta` | default `1f-4` | Learning rate for `:recurrent_stdp` |
| `sync` | default `true` | `CUDA.synchronize()` at end of step; host spike diagnostics only when true |
| `record_history` | default `true` | Write spike history; if false, covariance helpers may see stale/incomplete history |
| `use_device_noise` | default `false` | Host Gaussian noise upload; device RNG with host fallback if unavailable |

Recurrent reservoir weights are **not** trained under the default path.
Requires **CUDA.jl 6.x**. Local verification and CI workflows use **Julia 1.12**.

## OU-SDE Membrane Dynamics

```
Expand Down
2 changes: 1 addition & 1 deletion REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

### CI/CD

- [ ] All Julia versions pass (1.10, 1.11, 1.12)
- [ ] Julia 1.12 CI passes (CPU ubuntu-latest + GPU self-hosted when available)
- [ ] No Codacy warnings (SHA-pinned actions, no inline HTML in markdown)
- [ ] No unresolved bot review threads

Expand Down
37 changes: 36 additions & 1 deletion src/LiquidCortex.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ function __init__()
end
end

# Caller-facing validation (wrong kwargs / input size). Filtered from Sentry so
# unit tests and API misuse do not create issues. Internal faults (malformed
# CSC, CUDA OOM, unexpected ArgumentError from libs) remain captured.
struct LiquidCortexValidationError <: Exception
msg::String
end
Base.showerror(io::IO, e::LiquidCortexValidationError) =
print(io, "LiquidCortexValidationError: ", e.msg)

# Accept any thrown value (Julia allows non-Exception throws) so capture never
# raises MethodError and masks the original failure path.
#
Expand All @@ -65,12 +74,38 @@ end
# ensemble_step! before rethrow if capture were synchronous. Schedule capture
# asynchronously and only wait briefly; drop waiting (and leave the task
# running best-effort) if the queue is blocked.
@noinline function _should_capture_runtime_exception(@nospecialize(exc))
return !(exc isa LiquidCortexValidationError)
end

# Sentry.jl tags are process-global. Serialize tag+capture so concurrent async
# captures cannot cross-label each other's events.
const _sentry_capture_lock = ReentrantLock()

@noinline function _tag_runtime_exception!(@nospecialize(exc))
if exc isa CUDA.OutOfGPUMemoryError
Sentry.set_tag("gpu_failure", "true")
Sentry.set_tag("error_class", "gpu_oom")
elseif exc isa CUDA.CuError
Sentry.set_tag("gpu_failure", "true")
Sentry.set_tag("error_class", "cuda_error")
Comment thread
rmems marked this conversation as resolved.
else
Sentry.set_tag("gpu_failure", "false")
Sentry.set_tag("error_class", "runtime")
end
Comment thread
rmems marked this conversation as resolved.
return nothing
end

@noinline function _capture_runtime_exception(@nospecialize(exc), bt)
_sentry_enabled[] || return nothing
_should_capture_runtime_exception(exc) || return nothing
try
t = @async begin
try
Sentry.capture_exception([(exc, bt)])
lock(_sentry_capture_lock) do
_tag_runtime_exception!(exc)
Sentry.capture_exception([(exc, bt)])
end
catch sentry_error
@warn "LiquidCortex: Failed to capture exception in Sentry" exception=(sentry_error, catch_backtrace())
end
Expand Down
Loading