packages feed

hhlo-0.7.0.0: README.md

# HHLO — Haskell Frontend for StableHLO

HHLO is a Haskell library for building, compiling, and executing machine learning programs that target [StableHLO](https://github.com/openxla/stablehlo), the portable, versioned IR of the [OpenXLA](https://openxla.org/) ecosystem.

It lets you write ML models in pure Haskell with compile-time shape checking, compile them to CPU or GPU via the [PJRT](https://github.com/openxla/xla/blob/main/xla/pjrt/c/pjrt_c_api.h) C API, and even differentiate them automatically — all without leaving the type system.

```haskell
{-# LANGUAGE DataKinds, TypeApplications #-}
import HHLO.Session
import HHLO.EDSL.Ops
import HHLO.Autograd

-- Define a model, differentiate it, and run it on CPU in 6 lines.
main = withCPU $ \sess -> do
    let f x = sumAll =<< multiply x x
        gradMod = gradModule @'[3] @'F32 f
    compiled <- compile sess gradMod
    result   <- run sess compiled (hostFromList @'[3] @'F32 [1, 2, 3])
    print (hostToList result)   -- [2.0, 4.0, 6.0]
```

---

## Table of Contents

- [Why HHLO?](#why-hhlo)
- [Design Philosophy](#design-philosophy)
- [Features](#features)
  - [Type-Safe EDSL](#type-safe-edsl)
  - [Convenience Ops](#convenience-ops)
  - [Autograd](#autograd)
  - [Runtime & Hardware](#runtime--hardware)
  - [Control Flow & RNG](#control-flow--rng)
- [Quick Start](#quick-start)
- [Examples](#examples)
- [Installation](#installation)
- [Project Structure](#project-structure)
- [License](#license)

---

## Why HHLO?

Most ML frameworks trace Python code to build computation graphs. HHLO takes a different path: you write StableHLO directly in Haskell.

This means:

- **No Python runtime** — Your model is ordinary Haskell code.
- **Compile-time shape safety** — Matmul mismatches are type errors, not runtime failures.
- **Native autograd** — Reverse-mode differentiation is implemented as a Haskell library, not a C++ backend.
- **True portability** — StableHLO is a standardized, versioned IR; the same Haskell code runs on CPU, NVIDIA GPU, or any future PJRT backend.

---

## Design Philosophy

### Text Emission + PJRT

HHLO emits StableHLO MLIR text and hands it straight to `PJRT_Client_Compile`. This is the same compilation path used by JAX's C++ backend, but without the heavy dependency of building LLVM/MLIR from source.

### Phantom Types

Every tensor carries its shape and dtype as phantom type parameters:

```haskell
Tensor '[2, 3] 'F32   -- 2×3 matrix of Float32
Tensor '[4]    'F64   -- 4-element vector of Float64
```

Matmul, broadcast, and conv shapes are checked at compile time via type families. If the shapes don't match, GHC tells you before you ever load a PJRT plugin.

### Layered Architecture

HHLO is structured so you can use as much or as little abstraction as you need:

```
┌─────────────────────────────────────┐
│  Session (HHLO.Session)             │  One-liners: withCPU, compile, run
├─────────────────────────────────────┤
│  Autograd (HHLO.Autograd)           │  grad, vjp, gradModule — reverse-mode AD
├─────────────────────────────────────┤
│  EDSL (HHLO.EDSL.Ops)               │  Type-safe frontend: add, matmul, einsum, etc.
├─────────────────────────────────────┤
│  IR Builder (HHLO.IR.Builder)       │  Stateful monad for constructing MLIR
├─────────────────────────────────────┤
│  Pretty Printer (HHLO.IR.Pretty)    │  Emits StableHLO MLIR text
├─────────────────────────────────────┤
│  PJRT Runtime (HHLO.Runtime.*)      │  Compile → Execute on CPU or GPU
└─────────────────────────────────────┘
```

The high-level layers (`Session`, `Autograd`) eliminate PJRT boilerplate for the common case. The low-level layers (`IR.Builder`, `Pretty`, `Runtime`) remain available when you need full control.

---

## Features

### Type-Safe EDSL

The frontend provides 50+ typed ops covering arithmetic, linear algebra, reductions, data movement, and neural network primitives:

```haskell
-- Arithmetic
c <- add a b
d <- multiply a b
e <- matmul a b

-- Non-linear
y <- relu x
y <- sigmoid x
y <- softmax x

-- Reductions
s <- sumAll x                    -- reduce all dims → scalar
v <- reduceSumDim @0 x           -- reduce dim 0

-- Data movement
sliced <- slice x [(0, 2), (1, 3)]   -- extract sub-array
padded <- pad x 0 [(1, 1), (0, 0)]   -- pad with zeros
trans  <- transpose x [1, 0]         -- permute dimensions
```

Shape mismatches are caught at compile time. A matmul between a `[2,3]` and a `[4,5]` tensor is a type error, not a segfault.

### Convenience Ops

Beyond the raw StableHLO surface, HHLO provides higher-level combinators that compose primitive ops into familiar patterns:

| Op | What it does |
|---|---|
| `einsum "ij,jk->ik" a b` | Einstein summation (dispatches to `dotGeneral` + `transpose`) |
| `split dim n t` | Decompose a tensor into `n` equal slices along `dim` |
| `stack dim [t1, t2, ...]` | Concatenate tensors along a new axis `dim` |
| `productAll t` | Reduce all dimensions with `multiply` (like `sumAll` but product) |
| `productDim dims t` | Reduce specific dimensions with `multiply` |
| `topK k t` | Return the top-K elements (sort descending + slice) |

These are implemented as pure compositions of existing EDSL ops, so they inherit full autograd support automatically.

### Autograd

HHLO includes a native reverse-mode automatic differentiation engine that transforms StableHLO computation graphs into their gradients.

**Standalone modules** — produce a reusable `Module`:

```haskell
-- f(x) = sum(x²)   =>   grad f(x) = 2x
gradMod :: Module
gradMod = gradModule @'[3] @'F32 $ \x -> do
    sq <- multiply x x
    sumAll sq
```

**Multi-parameter gradients** — differentiate w.r.t. multiple inputs natively:

```haskell
-- g(x, y) = sum(x * y)   =>   (grad_x = y, grad_y = x)
(gradX, gradY) <- grad2 (\x y -> sumAll =<< multiply x y) xVal yVal
```

**Structured parameters with ParamTree** — train models with many weights
without manual pack/slice bookkeeping:

```haskell
{-# LANGUAGE DeriveGeneric #-}

data MLPParams = MLPParams
    { w1 :: Tensor '[2,2] 'F32
    , b1 :: Tensor '[2]   'F32
    , w2 :: Tensor '[1,2] 'F32
    , b2 :: Tensor '[1]   'F32
    } deriving (Generic)

instance ParamTree MLPParams

loss p x = do
    h  <- relu =<< add (matmul x (w1 p)) (b1 p)
    y  <- add (matmul h (w2 p)) (b2 p)
    diff <- sub y target
    sumAll =<< multiply diff diff

-- Returns an MLPParams of gradients
dParams <- gradWithParams loss params x
```

**In-place combinators** — use inside `buildModule` for composability:

```haskell
buildModule @1 @1 "loss_and_grad" $ \x -> do
    loss <- sumAll =<< multiply x x
    g    <- grad (\y -> sumAll (multiply y y)) x
    returnTuple2 loss g
```

**Vector-Jacobian products** — for non-scalar outputs:

```haskell
-- vjp f x seed = (Df(x))ᵀ · seed
vjpModule @'[3] @'[2] @'F32
    (\x -> do w <- constant @'[2,3] @'F32 1.0; matmul w x)
```

Supported ops: `add`, `subtract`, `multiply`, `divide`, `negate`, `exponential`, `log`, `sqrt`, `power`, `sine`, `cosine`, `tanh`, `abs`, `maximum`, `minimum`, `reshape`, `transpose`, `broadcast_in_dim`, `reduce` (sum), `dot`, `select`, `slice`, `pad`, `concatenate`, `convert`, `convolution`, `reduce_window`, and more.

Ops without gradient rules (e.g. `compare`, `floor`, `ceil`, `sort`) safely return zero gradients. Stubs (e.g. `gather`, `scatter`) error explicitly.

### Runtime & Hardware

**CPU & GPU**

The same Haskell code compiles to CPU via `withCPU` or to GPU via `withGPU`:

```haskell
withCPU $ \sess -> do ...   -- CPU plugin, works out of the box
withGPU $ \sess -> do ...   -- CUDA plugin, requires NVIDIA runtime libs
```

**Async Execution**

`HHLO.Runtime.Async` provides true non-blocking execution:

```haskell
bufs <- executeAsync api exec inputs
ready <- bufferReady api (head bufs)   -- poll
awaitBuffers api bufs                   -- block until done
```

**Multi-GPU Inference**

Run the same compiled model concurrently across multiple GPUs:

```haskell
compileWithOptions api client mlirText
    (defaultCompileOptions { optNumReplicas = numDevs })

executeReplicas api exec
    [ (gpu0, [bufA0, bufB0])
    , (gpu1, [bufA1, bufB1])
    , ...
    ]
```

**ForeignPtr Finalizers**

PJRT buffers and executables are managed by `ForeignPtr` finalizers. They are automatically destroyed when garbage-collected — no explicit cleanup required.

### Control Flow & RNG

**Multi-Value Control Flow**

`whileLoop2` / `conditional2` carry multiple typed tensors through loops and conditionals without manual packing:

```haskell
(resultCounter, resultSum) <- whileLoop2 counter0 sum0
    (\c s -> compare c limit "LT")
    (\c s -> do
        cNext <- add c one
        sNext <- add s cNext
        returnTuple2 cNext sNext)
```

**Random Number Generation**

```haskell
uniform  <- rngUniform a b      -- uniform in [a, b)
normal   <- rngNormal            -- standard normal (mean 0, std 1)
(newSt, bits) <- rngBitGenerator state   -- Threefry bit generator
```

---

## Quick Start

### 1. Download PJRT plugins

```bash
./pjrt_script.sh
```

This fetches `libpjrt_cpu.so` into `deps/pjrt/`. If you have an NVIDIA GPU, the CUDA plugin is also downloaded automatically.

### 2. Build

```bash
cabal build all
```

### 3. Run an example

```bash
# CPU — works out of the box
cabal run example-add --flag=examples

# Autograd
cabal run example-autograd-basic --flag=examples
```

### 4. Run tests

```bash
cabal test                    # 190 CPU tests
cabal test --test-options="-t HHLO+GPU"   # + 6 GPU integration tests
```

---

## Examples

Standalone examples live in `examples/` and cover arithmetic, neural networks, control flow, RNG, and autograd:

| # | Command | Description |
|---|---------|-------------|
| 1 | `example-add` | Element-wise `c = a + b` |
| 2 | `example-matmul` | 2×3 @ 3×2 matrix multiply |
| 3 | `example-chain-ops` | `(a + b) * (a - b)` |
| 4 | `example-async` | Async `executeAsync` + `relu` |
| 5 | `example-mlp` | 2-layer MLP |
| 6 | `example-mlp-batched` | Batched MLP |
| 7 | `example-tuple` | Multi-result `func.func` |
| 8 | `example-reduce` | `reduceSum` over all dimensions |
| 9 | `example-softmax` | 1-D and batched 2-D softmax |
| 10 | `example-conv2d` | NHWC conv2d |
| 11 | `example-batch-norm` | Batch norm inference |
| 12 | `example-while` | `whileLoop` count-up |
| 13 | `example-conditional` | `conditional` if-then-else |
| 14 | `example-gather` | `gather` rows from matrix |
| 15 | `example-scatter` | `scatter` replace into vector |
| 16 | `example-slice` | `slice` sub-array extraction |
| 17 | `example-pad` | `pad` with edge/interior padding |
| 18 | `example-dynamic-slice` | `dynamicSlice` runtime indices |
| 19 | `example-sort` | `sort` 1-D ascending |
| 20 | `example-select` | Element-wise ternary `select` |
| 21 | `example-map` | `map` with custom computation |
| 22 | `example-new-ops-smoke-test` | Smoke test for newer ops |
| 23 | `example-resnet` | ResNet-18 toy (8×8 input) |
| 24 | `example-alexnet` | AlexNet toy (16×16 input) |
| 25 | `example-transformer` | Transformer encoder (1×4×16) |
| 26 | `example-unet` | UNet segmentation toy (16×16) |
| 30 | `example-rng-uniform` | `rngUniform` random floats [0,1) |
| 31 | `example-rng-normal` | `rngNormal` standard normal distribution |
| 32 | `example-rng-bit-generator` | `rngBitGenerator` Threefry PRNG |
| 33 | `example-multi-value-loop` | `whileLoop2` with two loop-carried values |
| **34** | **`example-autograd-basic`** | **Gradient of `sum(x²)`** |
| **35** | **`example-autograd-linear`** | **Gradient of linear + MSE loss** |
| **36** | **`example-autograd-composite`** | **Gradient through ReLU + linear + sum** |
| **37** | **`example-autograd-multiparam`** | **`gradWithParams` on a record of weights** |
| 27 | `example-gpu-add` | GPU smoke test |
| 28 | `example-gpu-matmul-bench` | GPU 4096×4096 benchmark |
| 29 | `example-multi-gpu-inference` | Multi-GPU concurrent matmul |

> **Note:** All `example-*` executables are guarded by the `examples` flag in `hhlo.cabal` (defaults to `False`). Append `--flag=examples` to every `cabal run example-*` command.

### Writing your own model

```haskell
{-# LANGUAGE DataKinds, TypeApplications #-}

import HHLO.Session
import HHLO.EDSL.Ops
import HHLO.Autograd

-- A tiny model: predict y from x via a learned weight.
-- We want the gradient of the squared error.
main = withCPU $ \sess -> do
    let model x = do
            w <- constant @'[1] @'F32 2.0   -- fixed weight for demo
            b <- constant @'[1] @'F32 1.0
            y <- add =<< multiply w x =<< pure b
            tgt <- constant @'[1] @'F32 5.0
            diff <- sub y tgt
            sumAll =<< multiply diff diff

    let gradMod = gradModule @'[1] @'F32 model
    compiled <- compile sess gradMod
    result   <- run sess compiled (hostFromList @'[1] @'F32 [3.0])
    print (hostToList result)   -- [8.0]
```

---

## Installation

### System Requirements

- GHC 9.6+ and Cabal 3.10+
- Linux x86_64 (other platforms supported by PJRT artifacts may work)
- `curl`, `tar`, and standard C toolchain (`gcc` or `clang`)
- `libstdc++` and `libdl` (usually present on Linux)

### From Hackage

```cabal
build-depends: hhlo >= 0.5
```

Or:

```bash
cabal install hhlo
```

### GPU Setup

The PJRT CUDA plugin depends on **cuDNN**, **NCCL**, and **NVSHMEM**. If you already have them (e.g. via PyTorch or JAX):

```bash
./setup_gpu_env.sh
source ~/.bashrc
```

This auto-discovers the libraries and appends them to `~/.bashrc`. After that, GPU examples work directly:

```bash
cabal run example-gpu-add --flag=examples
cabal run example-gpu-matmul-bench --flag=examples
```

---

## Project Structure

```
.
├── app/                    # hhlo-demo executable
├── cbits/                  # C shim around PJRT C API
│   ├── pjrt_c_api.h        # Upstream PJRT header
│   ├── pjrt_shim.c         # Thin wrapper exposing flat C functions
│   └── pjrt_shim.h         # C header for the shim
├── deps/
│   └── pjrt/               # Downloaded PJRT plugins (.so files)
│       └── lib_symlinks/   # Compatibility symlinks for missing library versions
├── doc/                    # Architecture and design documents
├── examples/               # Standalone example programs (01–36)
├── src/HHLO/
│   ├── Autograd/           # Reverse-mode automatic differentiation
│   │   ├── Autograd.hs     # Public re-export module
│   │   ├── Core.hs         # BTensor (runtime-typed backward handles)
│   │   ├── Grad.hs         # grad, grad2, grad3, gradModule, gradModule2, gradModule3
│   │   ├── ParamTree.hs    # Generic pack/unpack for multi-parameter training
│   │   └── Rules.hs        # Per-op VJP rules (~30 ops)
│   ├── Core/Types.hs       # DType, Shape, HostType type families
│   ├── IR/
│   │   ├── AST.hs          # MLIR AST (Operation, Function, Module)
│   │   ├── Builder.hs      # Stateful Builder monad + Tensor/Tuple GADTs
│   │   └── Pretty.hs       # MLIR text pretty-printer
│   ├── EDSL/Ops.hs         # Type-safe frontend ops (50+ ops + convenience wrappers)
│   ├── ModuleBuilder.hs    # Typeclass-dispatched buildModuleN @M @K
│   ├── Session.hs          # High-level withCPU / withGPU / compile / run API
│   └── Runtime/
│       ├── PJRT/
│       │   ├── FFI.hs      # C FFI declarations
│       │   ├── Types.hs    # Opaque pointer newtypes + buffer type constants
│       │   ├── Error.hs    # PJRT error handling
│       │   └── Plugin.hs   # Backend-agnostic plugin loading (withPJRT)
│       ├── Device.hs       # Device enumeration & selection
│       ├── Compile.hs      # MLIR → PJRT executable (with CompileOptions)
│       ├── Execute.hs      # Synchronous + device-targeted + multi-GPU replica execution
│       ├── Async.hs        # Non-blocking execution with PJRT_Event
│       └── Buffer.hs       # Host↔device buffer transfers + metadata queries
├── test/
│   ├── Test/
│   │   ├── Autograd/       # Autograd golden & unit tests
│   │   │   ├── Grad.hs
│   │   │   └── Rules.hs
│   │   ├── EDSL/Ops.hs
│   │   ├── IR/
│   │   │   ├── Builder.hs
│   │   │   ├── Pretty.hs
│   │   │   ├── PrettyOps.hs
│   │   │   ├── PrettyNN.hs
│   │   │   └── PrettyControlFlow.hs
│   │   ├── Runtime/
│   │   │   ├── EndToEnd*.hs       # CPU E2E test modules
│   │   │   ├── EndToEndAutograd.hs # Numerical autograd verification
│   │   │   ├── EndToEndGPU.hs     # GPU availability test
│   │   │   ├── Buffer.hs
│   │   │   ├── BufferGPU.hs       # GPU buffer integration tests
│   │   │   ├── Async.hs
│   │   │   ├── AsyncGPU.hs        # GPU async tests
│   │   │   ├── MultiGPU.hs        # Multi-GPU inference scaling tests
│   │   │   └── Errors.hs
│   │   └── Utils.hs
│   └── Main.hs
├── hhlo.cabal
├── pjrt_script.sh          # Downloads PJRT plugins
├── setup_gpu_env.sh        # Auto-configures LD_LIBRARY_PATH for GPU
└── README.md
```

---

## License

MIT License — see [LICENSE](LICENSE).