hhlo 0.4.0.0 → 0.5.0.0
raw patch · 21 files changed
+2209/−299 lines, 21 filesnew-component:exe:example-autograd-basicnew-component:exe:example-autograd-compositenew-component:exe:example-autograd-linearPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ HHLO.Autograd.Core: BTensor :: !ValueId -> !TensorType -> BTensor
+ HHLO.Autograd.Core: [btType] :: BTensor -> !TensorType
+ HHLO.Autograd.Core: [btVid] :: BTensor -> !ValueId
+ HHLO.Autograd.Core: accumulate :: CotangentMap -> ValueId -> BTensor -> Builder CotangentMap
+ HHLO.Autograd.Core: babs :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: badd :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bbroadcastInDim :: BTensor -> [Int64] -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bcompareGE :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bconcatenate :: [BTensor] -> Int64 -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bconstant :: TensorType -> Double -> Builder BTensor
+ HHLO.Autograd.Core: bconvert :: BTensor -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bcos :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bdivide :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bdot :: BTensor -> BTensor -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bexp :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bfromTyped :: forall (s :: Shape) (d :: DType). (KnownShape s, KnownDType d) => Tensor s d -> BTensor
+ HHLO.Autograd.Core: blog :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bmaximum :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bminimum :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bmultiply :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bnegate :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bpad :: BTensor -> BTensor -> [Int64] -> [Int64] -> [Int64] -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: breduceSum :: BTensor -> [Int] -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: breshape :: BTensor -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bselect :: BTensor -> BTensor -> BTensor -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bsin :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bslice :: BTensor -> [Int64] -> [Int64] -> [Int64] -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: bsqrt :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: bsubtract :: BTensor -> BTensor -> Builder BTensor
+ HHLO.Autograd.Core: btanh :: BTensor -> Builder BTensor
+ HHLO.Autograd.Core: btoTyped :: forall (s :: Shape) (d :: DType). (KnownShape s, KnownDType d) => BTensor -> Tensor s d
+ HHLO.Autograd.Core: btranspose :: BTensor -> [Int64] -> TensorType -> Builder BTensor
+ HHLO.Autograd.Core: data BTensor
+ HHLO.Autograd.Core: instance GHC.Classes.Eq HHLO.Autograd.Core.BTensor
+ HHLO.Autograd.Core: instance GHC.Show.Show HHLO.Autograd.Core.BTensor
+ HHLO.Autograd.Core: reifyShape :: [Integer] -> (forall (s :: Shape). KnownShape s => Proxy s -> r) -> r
+ HHLO.Autograd.Core: type CotangentMap = Map ValueId BTensor
+ HHLO.Autograd.Grad: grad :: forall (s :: Shape) (d :: DType). (KnownShape s, KnownDType d) => (Tensor s d -> Builder (Tensor ('[] :: [Nat]) d)) -> Tensor s d -> Builder (Tensor s d)
+ HHLO.Autograd.Grad: gradModule :: forall (s :: Shape) (d :: DType). (KnownShape s, KnownDType d) => (Tensor s d -> Builder (Tensor ('[] :: [Nat]) d)) -> Module
+ HHLO.Autograd.Grad: vjp :: forall (s :: Shape) (t :: Shape) (d :: DType). (KnownShape s, KnownShape t, KnownDType d) => (Tensor s d -> Builder (Tensor t d)) -> Tensor s d -> Tensor t d -> Builder (Tensor s d)
+ HHLO.Autograd.Grad: vjpModule :: forall (s :: Shape) (t :: Shape) (d :: DType). (KnownShape s, KnownShape t, KnownDType d) => (Tensor s d -> Builder (Tensor t d)) -> Module
+ HHLO.Autograd.Rules: backwardStep :: Map ValueId BTensor -> Operation -> Builder (Map ValueId BTensor)
+ HHLO.EDSL.Ops: einsum :: forall (s1 :: Shape) (s2 :: Shape) (sOut :: Shape) (d :: DType). (KnownShape s1, KnownShape s2, KnownShape sOut, KnownDType d) => String -> Tensor s1 d -> Tensor s2 d -> Builder (Tensor sOut d)
+ HHLO.EDSL.Ops: productAll :: forall (s :: Shape) (d :: DType). (KnownShape s, KnownDType d) => Tensor s d -> Builder (Tensor ('[] :: [Nat]) d)
+ HHLO.EDSL.Ops: productDim :: forall (sFrom :: Shape) (sTo :: Shape) (d :: DType). (KnownShape sFrom, KnownShape sTo, KnownDType d) => [Int] -> Tensor sFrom d -> Builder (Tensor sTo d)
+ HHLO.EDSL.Ops: split :: forall (sIn :: Shape) (sOut :: Shape) (d :: DType). (KnownShape sIn, KnownShape sOut, KnownDType d) => Int64 -> Int64 -> Tensor sIn d -> Builder [Tensor sOut d]
+ HHLO.EDSL.Ops: stack :: forall (sIn :: Shape) (sOut :: Shape) (d :: DType). (KnownShape sIn, KnownShape sOut, KnownDType d) => Int64 -> [Tensor sIn d] -> Builder (Tensor sOut d)
+ HHLO.EDSL.Ops: topK :: forall (s :: Shape) (sOut :: Shape) (d :: DType). (KnownShape s, KnownShape sOut, KnownDType d) => Int64 -> Int64 -> Tensor s d -> Builder (Tensor sOut d)
Files
- CHANGELOG.md +22/−1
- README.md +270/−294
- examples/34-autograd-basic.hs +54/−0
- examples/35-autograd-linear.hs +62/−0
- examples/36-autograd-composite.hs +67/−0
- hhlo.cabal +47/−1
- src/HHLO/Autograd.hs +29/−0
- src/HHLO/Autograd/Core.hs +294/−0
- src/HHLO/Autograd/Grad.hs +195/−0
- src/HHLO/Autograd/Rules.hs +599/−0
- src/HHLO/EDSL/Ops.hs +206/−0
- src/HHLO/IR/Pretty.hs +2/−2
- test/Main.hs +6/−1
- test/Test/Autograd/Grad.hs +47/−0
- test/Test/Autograd/Rules.hs +49/−0
- test/Test/EDSL/Ops.hs +71/−0
- test/Test/Runtime/EndToEndAutograd.hs +74/−0
- test/Test/Runtime/EndToEndDataMovement.hs +14/−0
- test/Test/Runtime/EndToEndMatmul.hs +41/−0
- test/Test/Runtime/EndToEndReductions.hs +27/−0
- test/Test/Runtime/EndToEndShape.hs +33/−0
CHANGELOG.md view
@@ -47,7 +47,7 @@ * New comparison wrappers: `equal`, `notEqual`, `greaterThan`, `lessThanOrEqual`, `greaterThanOrEqual`. * Test count: 141 CPU tests + 6 GPU integration tests. -## 0.4.0.0 -- 2026-04-20+## 0.4.0.0 -- 2026-04-26 **BREAKING**: `HostType 'Bool` changed from `Bool` to `Word8` to match PJRT's PRED buffer transfer semantics.@@ -66,3 +66,24 @@ * Boolean logic ops: `logicalAnd`, `logicalOr`, `logicalNot`. * New dependency: `directory` (for plugin-path discovery in `withCPU`/`withGPU`). * Test count: 155 CPU tests + 6 GPU integration tests.+++## 0.5.0.0 -- 2026-04-27++* **Autograd** — reverse-mode automatic differentiation is now part of HHLO.+ New module `HHLO.Autograd` provides `grad` and `vjp` combinators that+ transform HHLO computation graphs into their gradients, producing new+ StableHLO modules that compile via PJRT. VJP rules cover ~25 ops including+ element-wise arithmetic, matmul, transpose, reshape, broadcast, reduce,+ slice, pad, concatenate, select, and more.+* New convenience ops:+ * `einsum` — Einstein summation via subscript strings (e.g. `"ij,jk->ik"`).+ Parses labels, computes batch/contracting dims, and emits the correct+ `stablehlo.dot_general` + optional `stablehlo.transpose`.+ * `split` — split a tensor into N equal parts along a dimension.+ * `stack` — stack N tensors along a new axis.+ * `productAll`, `productDim` — product reductions (mirrors `sumAll`/`reduceSumDim`).+ * `topK` — return top-K values along a dimension via `sort` + `slice`.+* Bug fix: `stablehlo.sort` now wraps its region in parentheses for PJRT+ v1.16.0 parser compatibility.+* Test count: 181 CPU tests + 6 GPU integration tests.
README.md view
@@ -1,21 +1,86 @@ # HHLO — Haskell Frontend for StableHLO -HHLO is a Haskell library and runtime for building, compiling, and executing machine learning programs targeting [StableHLO](https://github.com/openxla/stablehlo), the portable, versioned intermediate representation of the [OpenXLA](https://openxla.org/) ecosystem.+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. -Instead of replicating JAX's Python-based tracing infrastructure, HHLO generates StableHLO MLIR text directly from Haskell and compiles it to **CPU or GPU** via the [PJRT](https://github.com/openxla/xla/blob/main/xla/pjrt/c/pjrt_c_api.h) plugin interface.+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]+```+ --- -## Design+## Table of Contents -HHLO is structured in five layers:+- [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:++``` ┌─────────────────────────────────────┐-│ Convenience (HHLO.Session) │ One-liners: withCPU, compile, run+│ Session (HHLO.Session) │ One-liners: withCPU, compile, run ├─────────────────────────────────────┤-│ EDSL (HHLO.EDSL.Ops) │ Type-safe frontend: add, matmul, relu, etc.+│ 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@@ -24,86 +89,136 @@ └─────────────────────────────────────┘ ``` -**Text Emission + PJRT**+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. -The library emits StableHLO MLIR text directly and hands it to `PJRT_Client_Compile`. This is the same path used by JAX's C++ backend and avoids the heavy dependency of building LLVM/MLIR from source.+--- -**Phantom Types**+## Features -Every tensor carries its shape and dtype as phantom type parameters:+### Type-Safe EDSL++The frontend provides 50+ typed ops covering arithmetic, linear algebra, reductions, data movement, and neural network primitives:+ ```haskell-Tensor '[2, 3] 'F32 -- 2×3 matrix of Float32+-- 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 ```-Matmul, broadcast, and conv shapes are checked at compile time via type families. -**ForeignPtr Finalizers**+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. -PJRT buffers and executables are managed by `ForeignPtr` finalizers that automatically call `PJRT_Buffer_Destroy` and `PJRT_LoadedExecutable_Destroy` when values are garbage-collected. You can still let references drop out of scope without explicit cleanup.+### Convenience Ops -**Dynamic Output Counts**+Beyond the raw StableHLO surface, HHLO provides higher-level combinators that compose primitive ops into familiar patterns: -The runtime queries the compiled executable for its actual number of outputs via `PJRT_Executable_NumOutputs` instead of guessing or hardcoding a maximum.+| 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) | -**Async Execution**+These are implemented as pure compositions of existing EDSL ops, so they inherit full autograd support automatically. -`HHLO.Runtime.Async` provides true non-blocking execution: `executeAsync` returns buffer handles immediately, `bufferReady` polls for completion, and `awaitBuffers` blocks until device-side computation finishes.+### Autograd -**Device Enumeration & Selection**+HHLO includes a native reverse-mode automatic differentiation engine that transforms StableHLO computation graphs into their gradients. -`HHLO.Runtime.Device` lets you discover and select specific GPUs at runtime:+**Standalone modules** — produce a reusable `Module`:+ ```haskell-addressableDevices api client -- list all devices-deviceKind api dev -- "cpu" or "NVIDIA GeForce RTX 5090"-defaultGPUDevice api client -- first non-CPU device+-- f(x) = sum(x²) => grad f(x) = 2x+gradMod :: Module+gradMod = gradModule @'[3] @'F32 $ \x -> do+ sq <- multiply x x+ sumAll sq ``` -**Multi-GPU Inference Scaling**+**In-place combinators** — use inside `buildModule` for composability: -`HHLO.Runtime.Execute` provides `executeReplicas` for running the same compiled model concurrently across multiple GPUs: ```haskell-compileWithOptions api client mlirText- (defaultCompileOptions { optNumReplicas = numDevs })+buildModule @1 @1 "loss_and_grad" $ \x -> do+ loss <- sumAll =<< multiply x x+ g <- grad (\y -> sumAll (multiply y y)) x+ returnTuple2 loss g+``` --- Launch independent forward passes on all GPUs-executeReplicas api exec- [ (gpu0, [bufA0, bufB0])- , (gpu1, [bufA1, bufB1])- , ...- ]+**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) ``` -**Multi-Result Operations**+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`, and more. -The AST `Operation` type supports multiple results, enabling ops like `stablehlo.rng_bit_generator` and multi-value control flow:+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--- Two-result operation-(newState, output) <- rngBitGenerator state+withCPU $ \sess -> do ... -- CPU plugin, works out of the box+withGPU $ \sess -> do ... -- CUDA plugin, requires NVIDIA runtime libs ``` -**Convenience Layer**+**Async Execution** -`HHLO.ModuleBuilder` and `HHLO.Session` provide a high-level API that eliminates PJRT boilerplate for the common case:+`HHLO.Runtime.Async` provides true non-blocking execution: ```haskell-import HHLO.ModuleBuilder-import HHLO.Session+bufs <- executeAsync api exec inputs+ready <- bufferReady api (head bufs) -- poll+awaitBuffers api bufs -- block until done+``` --- Build + compile + run in four lines-main = withCPU $ \sess -> do- let modu = buildModule @2 @1 "mul" $ \x y -> multiply x y- compiled <- compile sess modu- result <- run sess compiled (hostFromList @'[2] [2.0, 3.0],- hostFromList @'[2] [4.0, 5.0])- print (hostToList result) -- [8.0, 15.0]+**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])+ , ...+ ] ``` -No `FuncArg`, no `natVal`, no `render`, no `toDeviceF32`, no explicit shape lists. The low-level API remains available for expert users who need full control.+**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--- Loop with two accumulators: counter and running sum (resultCounter, resultSum) <- whileLoop2 counter0 sum0 (\c s -> compare c limit "LT") (\c s -> do@@ -114,294 +229,157 @@ **Random Number Generation** -Three RNG primitives are exposed in the EDSL: ```haskell uniform <- rngUniform a b -- uniform in [a, b) normal <- rngNormal -- standard normal (mean 0, std 1) (newSt, bits) <- rngBitGenerator state -- Threefry bit generator ``` -**Extended Math Primitives**--Element-wise ops covering the full HBayesian requirements:-```haskell-y <- sqrt x -- square root-y <- rsqrt x -- reciprocal sqrt-y <- sin x -- sine-y <- cos x -- cosine-y <- tan x -- tangent-y <- pow x e -- element-wise power-y <- log1p x -- log(1+x)-y <- floor x -- floor-y <- ceil x -- ceiling-y <- sigmoid x -- 1 / (1 + exp(-x))-```--**Shape-Preserving Comparisons**--`compare` and its wrappers return `Tensor s 'Bool` (same shape as inputs), matching StableHLO semantics:-```haskell-mask <- equal x y -- element-wise equality-mask <- greaterThan x y -- element-wise >-mask <- lessThanOrEqual x y -- element-wise <=-```--Convenience ops for scalar manipulation:-```haskell-s <- sumAll x -- reduce all dimensions to scalar-v <- slice1 vec i -- extract scalar from 1-D tensor-packed <- pack2 a b -- pack two scalars into [2]-```- --- -## 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--HHLO is published on [Hackage](https://hackage.haskell.org/package/hhlo). You can add it directly to your `.cabal` file:--```cabal-build-depends: hhlo >= 0.4-```--Or with `cabal`:--```bash-cabal install hhlo-```--### Download PJRT Plugins+## Quick Start -Run the provided script to download prebuilt PJRT plugins:+### 1. Download PJRT plugins ```bash ./pjrt_script.sh ``` -This downloads `libpjrt_cpu.so` from the [zml/pjrt-artifacts](https://github.com/zml/pjrt-artifacts) nightly builds into `deps/pjrt/`. If you have an NVIDIA GPU with `nvidia-smi` available, the CUDA plugin is also fetched automatically.+This fetches `libpjrt_cpu.so` into `deps/pjrt/`. If you have an NVIDIA GPU, the CUDA plugin is also downloaded automatically. -### Build the Project+### 2. Build ```bash cabal build all ``` -This compiles the library, the demo, the examples, and the test suite.-------## Usage--### CPU (works out of the box)+### 3. Run an example ```bash+# CPU — works out of the box cabal run example-add --flag=examples-cabal test-``` -> **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.--### GPU (requires runtime libraries)--The PJRT CUDA plugin depends on NVIDIA runtime libraries: **cuDNN**, **NCCL**, and **NVSHMEM**. These are commonly available via conda, pip, or system packages.--If you already have them (e.g. via PyTorch or JAX installations), simply run:--```bash-./setup_gpu_env.sh-source ~/.bashrc+# Autograd+cabal run example-autograd-basic --flag=examples ``` -This idempotent script auto-discovers the libraries and appends them to `~/.bashrc`. After that, GPU examples work directly:+### 4. Run tests ```bash-cabal run example-gpu-add --flag=examples-cabal run example-gpu-matmul-bench --flag=examples-cabal run example-multi-gpu-inference --flag=examples+cabal test # 181 CPU tests+cabal test --test-options="-t HHLO+GPU" # + 6 GPU integration tests ``` --- -## EDSL Quick Start--### Convenience layer (recommended)--```haskell-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeApplications #-}+## Examples -import HHLO.ModuleBuilder-import HHLO.Session+Standalone examples live in `examples/` and cover arithmetic, neural networks, control flow, RNG, and autograd: --- Build a program: c = a + b-program = buildModule @2 @1 "add" $ \a b -> add a b+| # | 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** |+| 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 | -main = withCPU $ \sess -> do- compiled <- compile sess program- result <- run sess compiled- ( hostFromList @'[2,2] @'F32 [1, 2, 3, 4]- , hostFromList @'[2,2] @'F32 [5, 6, 7, 8]- )- print (hostToList result) -- [6.0, 8.0, 10.0, 12.0]-```+> **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. -### Low-level API (full control)+### Writing your own model ```haskell-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DataKinds, TypeApplications #-} -import HHLO.Core.Types+import HHLO.Session import HHLO.EDSL.Ops-import HHLO.IR.AST (FuncArg(..), TensorType(..))-import HHLO.IR.Builder-import HHLO.IR.Pretty-import qualified Data.Text as T---- Build a program: c = a + b-program :: Module-program = moduleFromBuilder @'[2,2] @'F32 "main"- [ FuncArg "a" (TensorType [2, 2] F32)- , FuncArg "b" (TensorType [2, 2] F32)- ]- $ do- a <- arg- b <- arg- c <- add a b- return c--main :: IO ()-main = T.putStrLn (render program)-```--Output:-```mlir-module {- func.func @main(%arg0: tensor<2x2xf32>, %arg1: tensor<2x2xf32>) -> tensor<2x2xf32> {- %0 = stablehlo.add %arg0, %arg1 : (tensor<2x2xf32>, tensor<2x2xf32>) -> tensor<2x2xf32>- return %0 : tensor<2x2xf32>- }-}-```+import HHLO.Autograd -### Running the Demo+-- 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 -```bash-cabal run hhlo-demo+ let gradMod = gradModule @'[1] @'F32 model+ compiled <- compile sess gradMod+ result <- run sess compiled (hostFromList @'[1] @'F32 [3.0])+ print (hostToList result) -- [8.0] ``` -The demo builds a `stablehlo.add` program via the EDSL, compiles it with PJRT CPU, creates F32 input buffers, executes, and reads back the result:--```-=== HHLO End-to-End Demo ===-Loading PJRT CPU plugin...-Plugin loaded.-...-Result: [6.0,8.0,10.0,12.0]-SUCCESS: Results match expected values!-```+--- -### Running Examples+## Installation -Standalone examples are provided in `examples/`:+### System Requirements -| # | Command | Description |-|---|---------|-------------|-| 1 | `cabal run example-add --flag=examples` | Element-wise `c = a + b` |-| 2 | `cabal run example-matmul --flag=examples` | 2×3 @ 3×2 matrix multiply |-| 3 | `cabal run example-chain-ops --flag=examples` | `(a + b) * (a - b)` |-| 4 | `cabal run example-async --flag=examples` | Async `executeAsync` + `relu` |-| 5 | `cabal run example-mlp --flag=examples` | 2-layer MLP |-| 6 | `cabal run example-mlp-batched --flag=examples` | Batched MLP |-| 7 | `cabal run example-tuple --flag=examples` | Multi-result `func.func` |-| 8 | `cabal run example-reduce --flag=examples` | `reduceSum` over all dimensions |-| 9 | `cabal run example-softmax --flag=examples` | 1-D and batched 2-D softmax |-| 10 | `cabal run example-conv2d --flag=examples` | NHWC conv2d |-| 11 | `cabal run example-batch-norm --flag=examples` | Batch norm inference |-| 12 | `cabal run example-while --flag=examples` | `whileLoop` count-up |-| 13 | `cabal run example-conditional --flag=examples` | `conditional` if-then-else |-| 14 | `cabal run example-gather --flag=examples` | `gather` rows from matrix |-| 15 | `cabal run example-scatter --flag=examples` | `scatter` replace into vector |-| 16 | `cabal run example-slice --flag=examples` | `slice` sub-array extraction |-| 17 | `cabal run example-pad --flag=examples` | `pad` with edge/interior padding |-| 18 | `cabal run example-dynamic-slice --flag=examples` | `dynamicSlice` runtime indices |-| 19 | `cabal run example-sort --flag=examples` | `sort` 1-D ascending |-| 20 | `cabal run example-select --flag=examples` | Element-wise ternary `select` |-| 21 | `cabal run example-map --flag=examples` | `map` with custom computation |-| 22 | `cabal run example-new-ops-smoke-test --flag=examples` | Smoke test for newer ops |-| 23 | `cabal run example-resnet --flag=examples` | ResNet-18 toy (8×8 input) |-| 24 | `cabal run example-alexnet --flag=examples` | AlexNet toy (16×16 input) |-| 25 | `cabal run example-transformer --flag=examples` | Transformer encoder (1×4×16) |-| 26 | `cabal run example-unet --flag=examples` | UNet segmentation toy (16×16) |-| 30 | `cabal run example-rng-uniform --flag=examples` | `rngUniform` random floats [0,1) |-| 31 | `cabal run example-rng-normal --flag=examples` | `rngNormal` standard normal distribution |-| 32 | `cabal run example-rng-bit-generator --flag=examples` | `rngBitGenerator` Threefry PRNG |-| 33 | `cabal run example-multi-value-loop --flag=examples` | `whileLoop2` with two loop-carried values |-| **27** | `cabal run example-gpu-add --flag=examples` | **GPU smoke test** |-| **28** | `cabal run example-gpu-matmul-bench --flag=examples` | **GPU 4096×4096 benchmark** |-| **29** | `cabal run example-multi-gpu-inference --flag=examples` | **Multi-GPU concurrent matmul** |+- 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 -## Tests+```cabal+build-depends: hhlo >= 0.5+``` -### CPU Tests (default)+Or: ```bash-cabal test+cabal install hhlo ``` -Runs **155 tests** across three tiers:--- **Tier 1 — Golden tests** — Verify rendered MLIR text for EDSL ops, IR constructs, NN layers, and control flow.-- **Tier 2 — End-to-end runtime tests** — Load the PJRT CPU plugin, compile StableHLO programs, execute them, and verify numerical results. Covers arithmetic, matmul, reductions, data movement, and NN ops.-- **Tier 3 — Runtime integration tests** — Buffer metadata queries, async execution, and error handling.+### GPU Setup -### GPU Tests+The PJRT CUDA plugin depends on **cuDNN**, **NCCL**, and **NVSHMEM**. If you already have them (e.g. via PyTorch or JAX): ```bash-HHLO_TEST_GPU=1 cabal test+./setup_gpu_env.sh+source ~/.bashrc ``` -Runs the full 155 CPU tests **plus** 6 additional GPU integration tests:--- `EndToEnd.GPU` — GPU availability and device enumeration-- `Runtime.BufferGPU` — Buffer round-trip and metadata queries on GPU-- `Runtime.AsyncGPU` — Async execution and `bufferReady` polling on GPU-- `Runtime.MultiGPU` — Concurrent `executeReplicas` across all GPUs--Sample output:-```-HHLO Tests- EDSL.Ops- Binary element-wise- add: OK- ...- EndToEnd.Arithmetic- relu: OK (0.02s)- ...- Runtime.Buffer- buffer round-trip f32: OK- Runtime.Async- buffer ready after sync execute: OK (0.02s)- EndToEnd.GPU- gpu available: OK- Runtime.BufferGPU- gpu buffer round-trip f32: OK- Runtime.AsyncGPU- gpu executeAsync + await: OK- Runtime.MultiGPU- execute replicas on all GPUs: OK+This auto-discovers the libraries and appends them to `~/.bashrc`. After that, GPU examples work directly: -All 147 tests passed (16.27s)+```bash+cabal run example-gpu-add --flag=examples+cabal run example-gpu-matmul-bench --flag=examples ``` ---@@ -419,14 +397,21 @@ │ └── pjrt/ # Downloaded PJRT plugins (.so files) │ └── lib_symlinks/ # Compatibility symlinks for missing library versions ├── doc/ # Architecture and design documents-├── examples/ # Standalone example programs (01–33)+├── 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, vjp, gradModule, vjpModule+│ │ └── Rules.hs # Per-op VJP rules (~25 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)+│ ├── 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@@ -434,13 +419,15 @@ │ │ ├── Error.hs # PJRT error handling │ │ └── Plugin.hs # Backend-agnostic plugin loading (withPJRT) │ ├── Device.hs # Device enumeration & selection-│ ├── Compile.hs # MLIR → PJRT executable-│ ├── Compile.hs # MLIR → PJRT executable (with `CompileOptions`)+│ ├── 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@@ -450,6 +437,7 @@ │ │ │ └── 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@@ -464,18 +452,6 @@ ├── setup_gpu_env.sh # Auto-configures LD_LIBRARY_PATH for GPU └── README.md ```-------<!-- ## Architecture Docs -->--<!-- The `doc/` directory contains detailed design documents: -->--<!-- | Document | Contents | -->-<!-- |----------|----------| -->-<!-- | `implementation-design.md` | Four-layer architecture and design decisions | -->-<!-- | `progress-and-remaining-work.md` | Current status, completed features, and backlog | -->-<!-- | `test-suite-documentation.md` | Test catalog and tier descriptions | --> ---
+ examples/34-autograd-basic.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 34: Basic Autograd — Gradient of sum(x²).+--+-- Demonstrates the simplest use of HHLO.Autograd:+-- f(x) = sum(x * x) => grad f(x) = 2 * x+--+-- Build and run with:+-- LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-autograd-basic++module Main where++import Prelude hiding (maximum)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Builder (Tensor, Builder)+import HHLO.Session+import HHLO.Autograd++main :: IO ()+main = withCPU $ \sess -> do+ putStrLn "=== Example 34: Basic Autograd ==="+ putStrLn "f(x) = sum(x * x) => grad f(x) = 2 * x"+ putStrLn ""++ -- Define a scalar-valued function.+ let f :: Tensor '[3] 'F32 -> Builder (Tensor '[] 'F32)+ f x = do+ sq <- multiply x x+ sumAll sq++ -- Compute its gradient as a standalone Module.+ let gradMod = gradModule @'[3] @'F32 f++ putStrLn "Generated gradient MLIR:"+ print gradMod+ putStrLn ""++ -- Compile and run.+ compiled <- compile sess gradMod+ let input = hostFromList @'[3] @'F32 [1.0, 2.0, 3.0]+ result <- run sess compiled input :: IO (HostTensor '[3] 'F32)++ putStrLn $ "Input: " ++ show (hostToList result)+ putStrLn $ "Expected: [2.0, 4.0, 6.0]"++ let resultList :: [Float]+ resultList = hostToList result+ if resultList == [2.0, 4.0, 6.0]+ then putStrLn "✓ PASS"+ else putStrLn "✗ FAIL"
+ examples/35-autograd-linear.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 35: Autograd — Gradient of a linear model + MSE loss.+--+-- We define: y = W * x + b (with fixed W, b)+-- and loss: L = (y - target)²+--+-- grad_L w.r.t. x = 2 * W * (y - target)+--+-- Build and run with:+-- LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-autograd-linear++module Main where++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Builder (Tensor, Builder)+import HHLO.Session+import HHLO.Autograd++main :: IO ()+main = withCPU $ \sess -> do+ putStrLn "=== Example 35: Autograd Linear + MSE ==="+ putStrLn "y = W*x + b, L = (y - target)^2"+ putStrLn "grad_x L = 2 * W * (y - target)"+ putStrLn ""++ -- Fixed parameters: W = 2.0, b = 1.0, target = 5.0+ -- f(x) = (2*x + 1 - 5)^2 = (2*x - 4)^2+ -- grad f(x) = 2 * 2 * (2*x - 4) = 8*x - 16+ -- At x = 3.0: grad = 8*3 - 16 = 8+ let f :: Tensor '[1] 'F32 -> Builder (Tensor '[] 'F32)+ f x = do+ w <- constant @'[1] @'F32 2.0+ b <- constant @'[1] @'F32 1.0+ tgt <- constant @'[1] @'F32 5.0+ wx <- multiply w x+ y <- add wx b+ diff <- sub y tgt+ sq <- multiply diff diff+ sumAll sq++ let gradMod = gradModule @'[1] @'F32 f++ putStrLn "Generated gradient MLIR:"+ print gradMod+ putStrLn ""++ compiled <- compile sess gradMod+ let input = hostFromList @'[1] @'F32 [3.0]+ result <- run sess compiled input :: IO (HostTensor '[1] 'F32)++ putStrLn $ "Input x: " ++ show (hostToList result)+ putStrLn $ "Expected grad: [8.0] (analytical: 8*3 - 16)"++ let resultList :: [Float]+ resultList = hostToList result+ if abs (head resultList - 8.0) < 0.001+ then putStrLn "✓ PASS"+ else putStrLn "✗ FAIL"
+ examples/36-autograd-composite.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 36: Autograd through a composite function (ReLU + linear + sum).+--+-- f(x) = sum( relu( w ⊙ x + b ) )+--+-- Demonstrates that autograd works through non-linear ops+-- (max/relu, multiply, add) and reduce (sumAll).+--+-- With w = [2, 3], b = [1, 1], x = [1, 1]:+-- z = w⊙x + b = [3, 4]+-- relu(z) = [3, 4]+-- f(x) = 7+--+-- grad w.r.t. x:+-- d(relu(z))/dz = [1, 1] (both positive)+-- dx = w ⊙ [1, 1] = [2, 3]+--+-- Build and run with:+-- LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-autograd-composite++module Main where++import Prelude hiding (maximum)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Builder (Tensor, Builder)+import HHLO.Session+import HHLO.Autograd++main :: IO ()+main = withCPU $ \sess -> do+ putStrLn "=== Example 36: Autograd Composite (ReLU + Linear + Sum) ==="+ putStrLn "f(x) = sum( relu( w * x + b ) )"+ putStrLn ""++ let f :: Tensor '[2] 'F32 -> Builder (Tensor '[] 'F32)+ f x = do+ w <- constant @'[2] @'F32 1.0+ b <- constant @'[2] @'F32 1.0+ wx <- multiply w x+ z <- add wx b+ zero <- constant @'[2] @'F32 0.0+ reluZ <- maximum z zero+ sumAll reluZ++ let gradMod = gradModule @'[2] @'F32 f++ putStrLn "Generated gradient MLIR:"+ print gradMod+ putStrLn ""++ compiled <- compile sess gradMod+ let input = hostFromList @'[2] @'F32 [1.0, 1.0]+ result <- run sess compiled input :: IO (HostTensor '[2] 'F32)++ putStrLn $ "Input x: " ++ show (hostToList result)+ putStrLn $ "Expected grad: [1.0, 1.0] (w=[1,1], b=[1,1])"++ let res :: [Float]+ res = hostToList result+ if all (\v -> abs (v - 1.0) < 0.001) res+ then putStrLn "✓ PASS"+ else putStrLn "✗ FAIL"
hhlo.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hhlo-version: 0.4.0.0+version: 0.5.0.0 synopsis: Haskell Frontend for StableHLO — type-safe ML inference on CPU and GPU description: HHLO is a Haskell library and runtime for building, compiling, and executing@@ -63,6 +63,10 @@ HHLO.EDSL.Ops HHLO.ModuleBuilder HHLO.Session+ HHLO.Autograd+ HHLO.Autograd.Core+ HHLO.Autograd.Grad+ HHLO.Autograd.Rules HHLO.Runtime.PJRT.FFI HHLO.Runtime.PJRT.Types HHLO.Runtime.PJRT.Error@@ -487,6 +491,8 @@ Test.IR.Builder Test.EDSL.Ops Test.ModuleBuilder+ Test.Autograd.Grad+ Test.Autograd.Rules Test.Runtime.EndToEnd Test.Runtime.EndToEndArithmetic Test.Runtime.EndToEndShape@@ -496,6 +502,7 @@ Test.Runtime.EndToEndDataMovement Test.Runtime.EndToEndMultiValue Test.Runtime.EndToEndSession+ Test.Runtime.EndToEndAutograd Test.Runtime.Buffer Test.Runtime.Async Test.Runtime.Errors@@ -558,6 +565,45 @@ executable example-multi-value-loop import: warnings main-is: 33-multi-value-loop.hs+ build-depends:+ base >= 4.18.2 && < 5,+ hhlo,+ vector >= 0.13 && < 0.14,+ text >= 2.0 && < 2.2+ hs-source-dirs: examples+ default-language: GHC2021+ if !flag(examples)+ buildable: False++executable example-autograd-basic+ import: warnings+ main-is: 34-autograd-basic.hs+ build-depends:+ base >= 4.18.2 && < 5,+ hhlo,+ vector >= 0.13 && < 0.14,+ text >= 2.0 && < 2.2+ hs-source-dirs: examples+ default-language: GHC2021+ if !flag(examples)+ buildable: False++executable example-autograd-linear+ import: warnings+ main-is: 35-autograd-linear.hs+ build-depends:+ base >= 4.18.2 && < 5,+ hhlo,+ vector >= 0.13 && < 0.14,+ text >= 2.0 && < 2.2+ hs-source-dirs: examples+ default-language: GHC2021+ if !flag(examples)+ buildable: False++executable example-autograd-composite+ import: warnings+ main-is: 36-autograd-composite.hs build-depends: base >= 4.18.2 && < 5, hhlo,
+ src/HHLO/Autograd.hs view
@@ -0,0 +1,29 @@+-- | Autograd-HHLO: Reverse-mode automatic differentiation for StableHLO.+--+-- This library provides 'grad' and 'vjp' combinators that transform HHLO+-- computation graphs into their gradients, producing new StableHLO modules+-- that compile via PJRT to CPU or GPU.+--+-- The primary safe entry points are 'gradModule' and 'vjpModule', which+-- produce standalone 'Module' values. The in-place 'grad' and 'vjp'+-- combinators can be used inside 'buildModule' for composability.+--+-- Example:+--+-- > import HHLO.ModuleBuilder+-- > import HHLO.Autograd+-- >+-- > -- f(x) = sum(x^2)+-- > -- grad f(x) = 2 * x+-- > gradMod = gradModule @'[3] @'F32 $ \x -> do+-- > sq <- multiply x x+-- > sumAll sq+module HHLO.Autograd+ ( module HHLO.Autograd.Core+ , module HHLO.Autograd.Grad+ , module HHLO.Autograd.Rules+ ) where++import HHLO.Autograd.Core+import HHLO.Autograd.Grad+import HHLO.Autograd.Rules
+ src/HHLO/Autograd/Core.hs view
@@ -0,0 +1,294 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++module HHLO.Autograd.Core+ ( BTensor(..)+ , CotangentMap+ , bconstant+ , badd+ , bsubtract+ , bmultiply+ , bdivide+ , bnegate+ , bexp+ , blog+ , bsqrt+ , bsin+ , bcos+ , btranspose+ , breshape+ , bbroadcastInDim+ , breduceSum+ , bdot+ , babs+ , btanh+ , bmaximum+ , bminimum+ , bselect+ , bslice+ , bpad+ , bconcatenate+ , bconvert+ , bcompareGE+ , btoTyped+ , bfromTyped+ , reifyShape+ , accumulate+ ) where++import Data.Int (Int64)+import Data.Proxy+import qualified Data.Text as T+import GHC.TypeLits+import qualified Data.Map.Strict as Map+import Data.Map.Strict (Map)++import HHLO.Core.Types+import HHLO.IR.AST+import HHLO.IR.Builder++-- ---------------------------------------------------------------------------+-- Backward tensor: runtime-typed tensor handle for the backward pass.+-- ---------------------------------------------------------------------------++-- | A tensor value used during the backward pass, carrying only runtime+-- shape and dtype information.+data BTensor = BTensor+ { btVid :: !ValueId+ , btType :: !TensorType+ } deriving (Eq, Show)++-- | Map from forward value ID to its accumulated cotangent.+type CotangentMap = Map ValueId BTensor++-- ---------------------------------------------------------------------------+-- Existential shape reification+-- ---------------------------------------------------------------------------++-- | Reify a runtime shape list into a type-level 'KnownShape' constraint.+reifyShape :: [Integer] -> (forall (s :: Shape). KnownShape s => Proxy s -> r) -> r+reifyShape [] k = k (Proxy @'[])+reifyShape (n : ns) k =+ case someNatVal (fromIntegral n) of+ Just (SomeNat (_ :: Proxy n)) ->+ reifyShape ns $ \(_ :: Proxy ns) ->+ k (Proxy @(n ': ns))+ Nothing -> error "autograd-hhlo: negative dimension in reifyShape"++-- ---------------------------------------------------------------------------+-- Conversions between BTensor and typed Tensor+-- ---------------------------------------------------------------------------++-- | Convert a typed 'Tensor' to a runtime-typed 'BTensor'.+bfromTyped :: forall s d. (KnownShape s, KnownDType d) => Tensor s d -> BTensor+bfromTyped (Tensor vid) = BTensor vid (tensorType (Proxy @s) (Proxy @d))++-- | Convert a 'BTensor' to a typed 'Tensor', checking that the runtime+-- type matches the expected compile-time type.+btoTyped :: forall s d. (KnownShape s, KnownDType d) => BTensor -> Tensor s d+btoTyped (BTensor vid ttype) =+ let expected = tensorType (Proxy @s) (Proxy @d)+ in if ttype == expected+ then Tensor vid+ else error $ T.unpack $+ "autograd-hhlo: type mismatch in btoTyped. Expected "+ <> T.pack (show expected) <> ", got " <> T.pack (show ttype)++-- ---------------------------------------------------------------------------+-- Cotangent accumulation+-- ---------------------------------------------------------------------------++-- | Add a new cotangent to an existing entry in the map, or insert it+-- if none exists yet.+accumulate :: CotangentMap -> ValueId -> BTensor -> Builder CotangentMap+accumulate cmap vid newBar =+ case Map.lookup vid cmap of+ Just existingBar -> do+ summed <- badd existingBar newBar+ return $! Map.insert vid summed cmap+ Nothing ->+ return $! Map.insert vid newBar cmap++-- ---------------------------------------------------------------------------+-- Primitive backward operations (emit raw StableHLO)+-- ---------------------------------------------------------------------------++bconstant :: TensorType -> Double -> Builder BTensor+bconstant t val = do+ let shp = ttShape t+ dt = ttDType t+ numElems = product shp+ vid <- emitOp "stablehlo.constant" [] []+ [AttrDenseElements shp dt (replicate (fromIntegral numElems) val)] t+ return (BTensor vid t)++badd :: BTensor -> BTensor -> Builder BTensor+badd (BTensor x t) (BTensor y _) = do+ vid <- emitOp "stablehlo.add" [x, y] [t, t] [] t+ return (BTensor vid t)++bsubtract :: BTensor -> BTensor -> Builder BTensor+bsubtract (BTensor x t) (BTensor y _) = do+ vid <- emitOp "stablehlo.subtract" [x, y] [t, t] [] t+ return (BTensor vid t)++bmultiply :: BTensor -> BTensor -> Builder BTensor+bmultiply (BTensor x t) (BTensor y _) = do+ vid <- emitOp "stablehlo.multiply" [x, y] [t, t] [] t+ return (BTensor vid t)++bdivide :: BTensor -> BTensor -> Builder BTensor+bdivide (BTensor x t) (BTensor y _) = do+ vid <- emitOp "stablehlo.divide" [x, y] [t, t] [] t+ return (BTensor vid t)++bnegate :: BTensor -> Builder BTensor+bnegate (BTensor x t) = do+ vid <- emitOp "stablehlo.negate" [x] [t] [] t+ return (BTensor vid t)++bexp :: BTensor -> Builder BTensor+bexp (BTensor x t) = do+ vid <- emitOp "stablehlo.exponential" [x] [t] [] t+ return (BTensor vid t)++blog :: BTensor -> Builder BTensor+blog (BTensor x t) = do+ vid <- emitOp "stablehlo.log" [x] [t] [] t+ return (BTensor vid t)++bsqrt :: BTensor -> Builder BTensor+bsqrt (BTensor x t) = do+ vid <- emitOp "stablehlo.sqrt" [x] [t] [] t+ return (BTensor vid t)++bsin :: BTensor -> Builder BTensor+bsin (BTensor x t) = do+ vid <- emitOp "stablehlo.sine" [x] [t] [] t+ return (BTensor vid t)++bcos :: BTensor -> Builder BTensor+bcos (BTensor x t) = do+ vid <- emitOp "stablehlo.cosine" [x] [t] [] t+ return (BTensor vid t)++-- | Transpose a BTensor using a permutation.+btranspose :: BTensor -> [Int64] -> TensorType -> Builder BTensor+btranspose (BTensor x inType) perm outType = do+ let permAttr = AttrIntList "permutation" perm+ vid <- emitOp "stablehlo.transpose" [x] [inType] [permAttr] outType+ return (BTensor vid outType)++-- | Reshape a BTensor.+breshape :: BTensor -> TensorType -> Builder BTensor+breshape (BTensor x inType) outType = do+ vid <- emitOp "stablehlo.reshape" [x] [inType] [] outType+ return (BTensor vid outType)++-- | Broadcast a BTensor to a new shape with explicit dimension mapping.+bbroadcastInDim :: BTensor -> [Int64] -> TensorType -> Builder BTensor+bbroadcastInDim (BTensor x inType) dims outType = do+ vid <- emitOp "stablehlo.broadcast_in_dim" [x] [inType]+ [AttrIntList "dims" (fromIntegral <$> dims)] outType+ return (BTensor vid outType)++-- | Element-wise selection between two BTensors based on a boolean predicate.+bselect :: BTensor -> BTensor -> BTensor -> TensorType -> Builder BTensor+bselect (BTensor p predType) (BTensor t _) (BTensor f _) outType = do+ vid <- emitOp "stablehlo.select" [p, t, f] [predType, outType, outType] [] outType+ return (BTensor vid outType)++-- | Slice a BTensor (forward operation wrapper).+bslice :: BTensor -> [Int64] -> [Int64] -> [Int64] -> TensorType -> Builder BTensor+bslice (BTensor x inType) start limit stride outType = do+ let startAttr = AttrRaw $ "start_indices = array<i64: " <> T.intercalate ", " ((T.pack . show) <$> start) <> ">"+ limitAttr = AttrRaw $ "limit_indices = array<i64: " <> T.intercalate ", " ((T.pack . show) <$> limit) <> ">"+ strideAttr = AttrRaw $ "strides = array<i64: " <> T.intercalate ", " ((T.pack . show) <$> stride) <> ">"+ vid <- emitOp "stablehlo.slice" [x] [inType] [startAttr, limitAttr, strideAttr] outType+ return (BTensor vid outType)++-- | Pad a BTensor with edge and interior padding.+bpad :: BTensor -> BTensor -> [Int64] -> [Int64] -> [Int64] -> TensorType -> Builder BTensor+bpad (BTensor x inType) (BTensor padVal padType) low high interior outType = do+ let lowAttr = AttrRaw $ "edge_padding_low = array<i64: " <> T.intercalate ", " ((T.pack . show) <$> low) <> ">"+ highAttr = AttrRaw $ "edge_padding_high = array<i64: " <> T.intercalate ", " ((T.pack . show) <$> high) <> ">"+ intAttr = AttrRaw $ "interior_padding = array<i64: " <> T.intercalate ", " ((T.pack . show) <$> interior) <> ">"+ vid <- emitOp "stablehlo.pad" [x, padVal] [inType, padType] [lowAttr, highAttr, intAttr] outType+ return (BTensor vid outType)++-- | Concatenate multiple BTensors along a dimension.+bconcatenate :: [BTensor] -> Int64 -> TensorType -> Builder BTensor+bconcatenate inputs dim outType = do+ let vids = map btVid inputs+ types = map btType inputs+ dimAttr = AttrInt "dimension" (fromIntegral dim)+ vid <- emitOp "stablehlo.concatenate" vids types [dimAttr] outType+ return (BTensor vid outType)++-- | Reduce-sum a BTensor over specified dimensions.+breduceSum :: BTensor -> [Int] -> TensorType -> Builder BTensor+breduceSum (BTensor x inType) dims outType = do+ let elemType = TensorType [] (ttDType inType)+ zeroVid <- emitOp "stablehlo.constant" [] []+ [AttrDenseElements [] (ttDType inType) [0.0]] elemType+ redBlock <- runBlockBuilder [elemType, elemType] $ do+ a <- arg @'[] @( 'F32)+ b <- arg @'[] @( 'F32)+ sumVid <- emitOp "stablehlo.add"+ [tensorValue a, tensorValue b]+ [elemType, elemType] [] elemType+ emitReturn [sumVid] [elemType]+ vid <- emitOpRegions "stablehlo.reduce"+ [x, zeroVid]+ [inType, elemType]+ [AttrRaw $ "dimensions = array<i64: " <> T.intercalate ", " [T.pack (show d) | d <- dims] <> ">"]+ [Region [redBlock]]+ outType+ return (BTensor vid outType)++-- | Matrix multiplication of two BTensors.+bdot :: BTensor -> BTensor -> TensorType -> Builder BTensor+bdot (BTensor x t1) (BTensor y t2) outType = do+ vid <- emitOp "stablehlo.dot" [x, y] [t1, t2] [] outType+ return (BTensor vid outType)++-- | Element-wise absolute value.+babs :: BTensor -> Builder BTensor+babs (BTensor x t) = do+ vid <- emitOp "stablehlo.abs" [x] [t] [] t+ return (BTensor vid t)++-- | Element-wise hyperbolic tangent.+btanh :: BTensor -> Builder BTensor+btanh (BTensor x t) = do+ vid <- emitOp "stablehlo.tanh" [x] [t] [] t+ return (BTensor vid t)++-- | Element-wise maximum.+bmaximum :: BTensor -> BTensor -> Builder BTensor+bmaximum (BTensor x t1) (BTensor y t2) = do+ vid <- emitOp "stablehlo.maximum" [x, y] [t1, t2] [] t1+ return (BTensor vid t1)++-- | Element-wise minimum.+bminimum :: BTensor -> BTensor -> Builder BTensor+bminimum (BTensor x t1) (BTensor y t2) = do+ vid <- emitOp "stablehlo.minimum" [x, y] [t1, t2] [] t1+ return (BTensor vid t1)++-- | Type conversion.+bconvert :: BTensor -> TensorType -> Builder BTensor+bconvert (BTensor x inType) outType = do+ vid <- emitOp "stablehlo.convert" [x] [inType] [] outType+ return (BTensor vid outType)++-- | Greater-than-or-equal comparison (returns boolean tensor).+bcompareGE :: BTensor -> BTensor -> Builder BTensor+bcompareGE (BTensor x t1) (BTensor y t2) = do+ let boolType = TensorType (ttShape t1) Bool+ vid <- emitOp "stablehlo.compare" [x, y] [t1, t2]+ [AttrRaw "comparison_direction = #stablehlo<comparison_direction GE>"] boolType+ return (BTensor vid boolType)
+ src/HHLO/Autograd/Grad.hs view
@@ -0,0 +1,195 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++module HHLO.Autograd.Grad+ ( grad+ , gradModule+ , vjp+ , vjpModule+ ) where++import Data.List (foldl')+import Data.Proxy+import Data.Foldable (foldlM)+import qualified Data.Map.Strict as Map+import Data.Map.Strict (Map)+import HHLO.Core.Types+import HHLO.EDSL.Ops (constant)+import HHLO.IR.AST+import HHLO.IR.Builder++import HHLO.Autograd.Core+import HHLO.Autograd.Rules++-- ---------------------------------------------------------------------------+-- Safe API: gradModule / vjpModule+-- ---------------------------------------------------------------------------++-- | Compute the gradient of a scalar-valued function as a standalone+-- 'Module'.+--+-- The resulting module takes a single tensor argument and returns its+-- gradient (a tensor of the same shape).+gradModule :: forall s d.+ (KnownShape s, KnownDType d)+ => (Tensor s d -> Builder (Tensor '[] d))+ -> Module+gradModule f = Module [gradFunction]+ where+ inType = tensorType (Proxy @s) (Proxy @d)+ arg0 = FuncArg "arg0" inType++ -- Capture the forward trace in isolation.+ forwardFunc :: Function+ forwardFunc = runBuilder @'[] @d "forward" [arg0] $ do+ input <- arg @s @d+ f input++ forwardOps :: [Operation]+ forwardOps = funcBody forwardFunc++ -- Build the combined forward+backward function.+ gradFunction :: Function+ gradFunction = runBuilder @s @d "main" [arg0] $ do+ input <- arg @s @d+ y <- f input++ -- Seed cotangent: 1.0 scalar.+ seed <- constant @'[] @d 1.0+ let initMap = Map.singleton (tensorValue y) (bfromTyped seed)++ -- Backpropagate through forward ops in reverse execution order.+ finalMap <- foldlBackward backwardStep initMap (reverse forwardOps)++ -- Extract gradient w.r.t. input.+ case Map.lookup (tensorValue input) finalMap of+ Just bt -> return (btoTyped @s bt)+ Nothing -> error "autograd-hhlo: gradient not found for input"++-- | Vector-Jacobian product as a standalone 'Module'.+vjpModule :: forall s t d.+ (KnownShape s, KnownShape t, KnownDType d)+ => (Tensor s d -> Builder (Tensor t d))+ -> Module+vjpModule f = Module [vjpFunction]+ where+ inType = tensorType (Proxy @s) (Proxy @d)+ seedType = tensorType (Proxy @t) (Proxy @d)+ arg0 = FuncArg "arg0" inType+ arg1 = FuncArg "arg1" seedType++ forwardFunc :: Function+ forwardFunc = runBuilder @t @d "forward" [arg0] $ do+ input <- arg @s @d+ f input++ forwardOps :: [Operation]+ forwardOps = funcBody forwardFunc++ vjpFunction :: Function+ vjpFunction = runBuilder @s @d "main" [arg0, arg1] $ do+ input <- arg @s @d+ seed <- arg @t @d+ _y <- f input++ let initMap = Map.singleton (tensorValue _y) (bfromTyped seed)+ finalMap <- foldlBackward backwardStep initMap (reverse forwardOps)++ case Map.lookup (tensorValue input) finalMap of+ Just bt -> return (btoTyped @s bt)+ Nothing -> error "autograd-hhlo: vjp gradient not found for input"++-- ---------------------------------------------------------------------------+-- In-place API: grad / vjp+-- ---------------------------------------------------------------------------++-- | Reverse-mode gradient combinator that works inside an existing+-- 'Builder' context.+--+-- This builds the gradient module in isolation and inlines its operations+-- into the current builder, remapping value IDs so they remain globally+-- consistent.+--+-- Example:+-- > buildModule @1 @1 "grad_f" $ \x -> grad (\y -> sumAll (multiply y y)) x+grad :: forall s d.+ (KnownShape s, KnownDType d)+ => (Tensor s d -> Builder (Tensor '[] d))+ -> Tensor s d+ -> Builder (Tensor s d)+grad f input = do+ let gradMod = gradModule f+ gradFunc = head (moduleFunctions gradMod)+ gradVid <- inlineFunction gradFunc (Map.singleton (ValueId (-1)) (tensorValue input))+ return (Tensor gradVid)++-- | Vector-Jacobian product inside an existing 'Builder' context.+vjp :: forall s t d.+ (KnownShape s, KnownShape t, KnownDType d)+ => (Tensor s d -> Builder (Tensor t d))+ -> Tensor s d+ -> Tensor t d+ -> Builder (Tensor s d)+vjp f input seed = do+ let vjpMod = vjpModule f+ vjpFunc = head (moduleFunctions vjpMod)+ gradVid <- inlineFunction vjpFunc $ Map.fromList+ [ (ValueId (-1), tensorValue input)+ , (ValueId (-2), tensorValue seed)+ ]+ return (Tensor gradVid)++-- ---------------------------------------------------------------------------+-- Internal helpers+-- ---------------------------------------------------------------------------++-- | Fold a backward step over a list of forward operations, threading the+-- cotangent map through the Builder monad.+foldlBackward :: (Map ValueId BTensor -> Operation -> Builder (Map ValueId BTensor))+ -> Map ValueId BTensor+ -> [Operation]+ -> Builder (Map ValueId BTensor)+foldlBackward _ cmap [] = return cmap+foldlBackward step cmap (op:ops) = do+ cmap' <- step cmap op+ foldlBackward step cmap' ops++-- | Inline a function's body operations into the current builder,+-- remapping value IDs so they don't conflict with existing values.+-- The argument mapping maps function argument IDs (negative) to the+-- current builder's actual value IDs.+inlineFunction :: Function -> Map ValueId ValueId -> Builder ValueId+inlineFunction func argMap = do+ finalMap <- foldlM inlineOp argMap (funcBody func)+ let retId = head (funcReturnVids func)+ return $ Map.findWithDefault retId retId finalMap+ where+ inlineOp :: Map ValueId ValueId -> Operation -> Builder (Map ValueId ValueId)+ inlineOp idMap op = do+ let remap vid = Map.findWithDefault vid vid idMap+ newOperands = map remap (opOperands op)+ newRegions = map (remapRegion idMap) (opRegions op)+ newVids <- emitOpRegionsN (opName op) newOperands (opOperandTypes op) (opAttributes op) newRegions (opResultTypes op)+ let updates = zip (opResults op) newVids+ return $ foldl' (\m (old, new) -> Map.insert old new m) idMap updates++ remapRegion :: Map ValueId ValueId -> Region -> Region+ remapRegion idMap (Region blocks) = Region (map (remapBlock idMap) blocks)++ remapBlock :: Map ValueId ValueId -> Block -> Block+ remapBlock idMap (Block blockArgs blockOps) =+ Block blockArgs (map (remapOp idMap) blockOps)++ remapOp :: Map ValueId ValueId -> Operation -> Operation+ remapOp idMap op =+ let remapOpVid vid = Map.findWithDefault vid vid idMap+ in op+ { opOperands = map remapOpVid (opOperands op)+ , opResults = map remapOpVid (opResults op)+ }++ -- Note: block arguments are local to the block and use negative IDs+ -- allocated by runBlockBuilder. They are self-contained and don't+ -- need remapping because they stay within their block scope.
+ src/HHLO/Autograd/Rules.hs view
@@ -0,0 +1,599 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}++module HHLO.Autograd.Rules+ ( backwardStep+ ) where++import Data.Int (Int64)+import Data.List (sortOn)+import Data.Maybe (isNothing)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Map.Strict as Map+import Data.Map.Strict (Map)++import HHLO.IR.AST+import HHLO.IR.Builder++import HHLO.Autograd.Core++-- ---------------------------------------------------------------------------+-- Main backward step dispatcher+-- ---------------------------------------------------------------------------++-- | Process one forward operation in reverse order, propagating cotangents.+backwardStep :: Map ValueId BTensor -> Operation -> Builder (Map ValueId BTensor)+backwardStep cmap op+ | all isNothing resultBars = return cmap+ | otherwise = case opName op of+ "stablehlo.add" -> vjpAdd op resultBars cmap+ "stablehlo.subtract" -> vjpSubtract op resultBars cmap+ "stablehlo.multiply" -> vjpMultiply op resultBars cmap+ "stablehlo.divide" -> vjpDivide op resultBars cmap+ "stablehlo.negate" -> vjpNegate op resultBars cmap+ "stablehlo.exponential" -> vjpExponential op resultBars cmap+ "stablehlo.log" -> vjpLog op resultBars cmap+ "stablehlo.sqrt" -> vjpSqrt op resultBars cmap+ "stablehlo.sine" -> vjpSine op resultBars cmap+ "stablehlo.cosine" -> vjpCosine op resultBars cmap+ "stablehlo.power" -> vjpPower op resultBars cmap+ "stablehlo.reshape" -> vjpReshape op resultBars cmap+ "stablehlo.transpose" -> vjpTranspose op resultBars cmap+ "stablehlo.broadcast_in_dim" -> vjpBroadcastInDim op resultBars cmap+ "stablehlo.reduce" -> vjpReduce op resultBars cmap+ "stablehlo.dot" -> vjpDot op resultBars cmap+ "stablehlo.select" -> vjpSelect op resultBars cmap+ "stablehlo.slice" -> vjpSlice op resultBars cmap+ "stablehlo.pad" -> vjpPad op resultBars cmap+ "stablehlo.concatenate" -> vjpConcatenate op resultBars cmap+ "stablehlo.gather" -> vjpGather op resultBars cmap+ "stablehlo.scatter" -> vjpScatter op resultBars cmap+ "stablehlo.convert" -> vjpConvert op resultBars cmap+ "stablehlo.abs" -> vjpAbs op resultBars cmap+ "stablehlo.maximum" -> vjpMaximum op resultBars cmap+ "stablehlo.minimum" -> vjpMinimum op resultBars cmap+ "stablehlo.tanh" -> vjpTanh op resultBars cmap+ "stablehlo.constant" -> return cmap -- constants have zero gradient+ "stablehlo.return" -> return cmap -- internal terminator+ "stablehlo.compare" -> return cmap -- comparisons have zero gradient+ "stablehlo.floor" -> return cmap -- non-differentiable+ "stablehlo.ceil" -> return cmap -- non-differentiable+ "stablehlo.sort" -> error "autograd-hhlo: sort/topK is not differentiable"+ _ -> error $ T.unpack $ "autograd-hhlo: no VJP rule for " <> opName op+ where+ resultBars = map (\r -> Map.lookup r cmap) (opResults op)++-- ---------------------------------------------------------------------------+-- VJP helpers+-- ---------------------------------------------------------------------------++getResultBar :: [Maybe BTensor] -> Maybe BTensor+getResultBar [mb] = mb+getResultBar _ = Nothing++operandBT :: Operation -> Int -> BTensor+operandBT op i = BTensor (opOperands op !! i) (opOperandTypes op !! i)++-- ---------------------------------------------------------------------------+-- Arithmetic VJP rules+-- ---------------------------------------------------------------------------++vjpAdd :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpAdd op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ yVid = opOperands op !! 1+ cmap' <- accumulate cmap xVid bar+ accumulate cmap' yVid bar+ Nothing -> return cmap++vjpSubtract :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpSubtract op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ yVid = opOperands op !! 1+ negBar <- bnegate bar+ cmap' <- accumulate cmap xVid bar+ accumulate cmap' yVid negBar+ Nothing -> return cmap++vjpMultiply :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpMultiply op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ y = operandBT op 1+ dx <- bmultiply bar y+ dy <- bmultiply bar x+ cmap' <- accumulate cmap (btVid x) dx+ accumulate cmap' (btVid y) dy+ Nothing -> return cmap++vjpDivide :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpDivide op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ y = operandBT op 1+ -- dx = bar / y+ dx <- bdivide bar y+ -- dy = -bar * x / (y * y)+ negBar <- bnegate bar+ t1 <- bmultiply negBar x+ ySq <- bmultiply y y+ dy <- bdivide t1 ySq+ cmap' <- accumulate cmap (btVid x) dx+ accumulate cmap' (btVid y) dy+ Nothing -> return cmap++vjpNegate :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpNegate op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ negBar <- bnegate bar+ accumulate cmap (opOperands op !! 0) negBar+ Nothing -> return cmap++-- ---------------------------------------------------------------------------+-- Math VJP rules+-- ---------------------------------------------------------------------------++vjpExponential :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpExponential op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ -- Forward result y = exp(x), so dy/dx = exp(x) = y.+ -- We need the forward result value. For now, recompute exp(x).+ let x = operandBT op 0+ y <- bexp x+ dx <- bmultiply bar y+ accumulate cmap (btVid x) dx+ Nothing -> return cmap++vjpLog :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpLog op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ -- dx = bar / x+ dx <- bdivide bar x+ accumulate cmap (btVid x) dx+ Nothing -> return cmap++vjpSqrt :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpSqrt op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ -- dx = bar / (2 * sqrt(x))+ two <- bconstant (btType x) 2.0+ s <- bsqrt x+ t1 <- bmultiply two s+ dx <- bdivide bar t1+ accumulate cmap (btVid x) dx+ Nothing -> return cmap++vjpSine :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpSine op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ c <- bcos x+ dx <- bmultiply bar c+ accumulate cmap (btVid x) dx+ Nothing -> return cmap++vjpCosine :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpCosine op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ s <- bsin x+ negS <- bnegate s+ dx <- bmultiply bar negS+ accumulate cmap (btVid x) dx+ Nothing -> return cmap++vjpPower :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpPower op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ y = operandBT op 1+ -- For z = x^y:+ -- dz/dx = y * x^(y-1)+ -- dz/dy = x^y * log(x)+ one <- bconstant (btType y) 1.0+ yMinus1 <- bsubtract y one+ xPow <- bpower x yMinus1+ dx <- bmultiply y xPow+ z <- bpower x y+ logX <- blog x+ dy <- bmultiply z logX+ dx' <- bmultiply bar dx+ dy' <- bmultiply bar dy+ cmap' <- accumulate cmap (btVid x) dx'+ accumulate cmap' (btVid y) dy'+ Nothing -> return cmap++-- Helper for power (not exported, used only inside VJP)+bpower :: BTensor -> BTensor -> Builder BTensor+bpower (BTensor x t) (BTensor y _) = do+ vid <- emitOp "stablehlo.power" [x, y] [t, t] [] t+ return (BTensor vid t)++-- ---------------------------------------------------------------------------+-- Shape manipulation VJP rules+-- ---------------------------------------------------------------------------++vjpReshape :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpReshape op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ -- Gradient is a reshape back to the input shape.+ dx <- breshape bar xType+ accumulate cmap xVid dx+ Nothing -> return cmap++vjpTranspose :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpTranspose op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ -- Extract permutation from attributes.+ perm <- case findPermutation (opAttributes op) of+ Just p -> return p+ Nothing -> error "autograd-hhlo: transpose missing permutation attribute"+ -- Inverse permutation.+ let invPerm = invertPerm perm+ dx <- btranspose bar invPerm xType+ accumulate cmap xVid dx+ Nothing -> return cmap+ where+ findPermutation :: [Attribute] -> Maybe [Int64]+ findPermutation [] = Nothing+ findPermutation (AttrIntList "permutation" p : _) = Just p+ findPermutation (_ : attrs) = findPermutation attrs++ invertPerm :: [Int64] -> [Int64]+ invertPerm perm = map fst $ sortOn snd $ zip ([0 ..] :: [Int64]) perm++vjpBroadcastInDim :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpBroadcastInDim op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ outType = head (opResultTypes op)+ -- Extract broadcast dims.+ dims <- case findDims (opAttributes op) of+ Just d -> return d+ Nothing -> error "autograd-hhlo: broadcast_in_dim missing dims attribute"+ -- Gradient: reduce over the broadcasted dimensions.+ let outRank = length (ttShape outType) :: Int+ broadcastDims = map fromIntegral dims :: [Int]+ reduceDims = filter (`notElem` broadcastDims) [0 .. outRank - 1]+ if null reduceDims+ then accumulate cmap xVid bar+ else do+ dx <- breduceSum bar reduceDims xType+ accumulate cmap xVid dx+ Nothing -> return cmap+ where+ findDims [] = Nothing+ findDims (AttrIntList "dims" d : _) = Just d+ findDims (_ : attrs) = findDims attrs++-- ---------------------------------------------------------------------------+-- Reduction VJP rules+-- ---------------------------------------------------------------------------++vjpReduce :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpReduce op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ -- We only support sum reductions for now.+ -- Detect sum by looking for stablehlo.add in the reduction region.+ let isSum = any regionHasAdd (opRegions op)+ regionHasAdd (Region blocks) = any blockHasAdd blocks+ blockHasAdd (Block _ ops) = any (\o -> opName o == "stablehlo.add") ops+ if not isSum+ then error "autograd-hhlo: only sum reductions are supported"+ else do+ dims <- case findDimensions (opAttributes op) of+ Just d -> return $ map fromIntegral d+ Nothing -> error "autograd-hhlo: reduce missing dimensions attribute"+ -- Gradient: broadcast the cotangent back to the input shape.+ dx <- broadcastLike bar xType dims+ accumulate cmap xVid dx+ Nothing -> return cmap+ where+ findDimensions [] = Nothing+ findDimensions (AttrRaw raw : _) =+ if "dimensions" `T.isPrefixOf` T.strip raw+ then Just $ parseIntList raw+ else Nothing+ findDimensions (AttrIntList "dimensions" dims : _) = Just dims+ findDimensions (_ : attrs) = findDimensions attrs++ parseIntList raw =+ let inner = T.takeWhile (/= '>') $ T.dropWhile (/= '<') raw+ withoutLt = T.dropWhile (== '<') inner+ withoutPrefix = if "i64:" `T.isPrefixOf` withoutLt then T.drop 4 withoutLt else withoutLt+ nums = T.splitOn "," withoutPrefix+ in map (read . T.unpack . T.strip) nums++ -- Broadcast a reduced cotangent back to the original shape.+ broadcastLike :: BTensor -> TensorType -> [Int] -> Builder BTensor+ broadcastLike bar inType dims = do+ let inRank = length (ttShape inType)+ -- Build broadcast dims: map each reduced dim to its position.+ -- For reduce over dims [0,1] of a 3-D tensor, we broadcast+ -- the scalar/shape to the original shape.+ -- We use broadcast_in_dim with dims = remaining_dims.+ remainingDims = filter (`notElem` dims) [0 .. inRank - 1]+ if length remainingDims == inRank+ then return bar -- No reduction happened+ else do+ bbroadcastInDim bar (map fromIntegral remainingDims) inType++-- ---------------------------------------------------------------------------+-- Linear algebra VJP rules+-- ---------------------------------------------------------------------------++vjpDot :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpDot op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ y = operandBT op 1+ xType = btType x+ yType = btType y+ -- For C = A @ B:+ -- dA = dC @ B^T+ -- dB = A^T @ dC+ -- We need to transpose the appropriate dimensions.+ -- For simplicity, assume standard 2-D matmul.+ let xShape = ttShape xType+ yShape = ttShape yType+ if length xShape == 2 && length yShape == 2+ then do+ bT <- btranspose y [1, 0] (TensorType (reverse $ ttShape yType) (ttDType yType))+ aT <- btranspose x [1, 0] (TensorType (reverse xShape) (ttDType xType))+ da <- bdot bar bT xType+ db <- bdot aT bar yType+ cmap' <- accumulate cmap (btVid x) da+ accumulate cmap' (btVid y) db+ else+ error "autograd-hhlo: dot VJP only supports 2-D matrices for now"+ Nothing -> return cmap++-- ---------------------------------------------------------------------------+-- Selection VJP rule+-- ---------------------------------------------------------------------------++vjpSelect :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpSelect op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let predVid = opOperands op !! 0+ trueVid = opOperands op !! 1+ falseVid = opOperands op !! 2+ predType = opOperandTypes op !! 0+ valType = opOperandTypes op !! 1+ zero <- bconstant valType 0.0+ -- dx = select(pred, bar, 0)+ dx <- bselect (BTensor predVid predType) bar zero valType+ -- dy = select(pred, 0, bar)+ dy <- bselect (BTensor predVid predType) zero bar valType+ cmap' <- accumulate cmap trueVid dx+ accumulate cmap' falseVid dy+ Nothing -> return cmap++-- ---------------------------------------------------------------------------+-- Slice VJP rule+-- ---------------------------------------------------------------------------++vjpSlice :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpSlice op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ (start, limit, stride) = parseSliceAttrs (opAttributes op)+ xShape = ttShape xType+ rank = length xShape+ low = start+ isUnitStride = all (== 1) stride+ if isUnitStride+ then do+ let high' = map fromIntegral (zipWith (-) xShape (map fromIntegral limit :: [Integer])) :: [Int64]+ interior = replicate rank (0 :: Int64)+ zero <- bconstant xType 0.0+ dx <- bpad bar zero low high' interior xType+ accumulate cmap xVid dx+ else do+ -- General stride: use interior padding = stride - 1+ let start' = map fromIntegral start :: [Integer]+ limit' = map fromIntegral limit :: [Integer]+ stride' = map fromIntegral stride :: [Integer]+ high' = map fromIntegral (zipWith (-) xShape (zipWith (+) start' (zipWith (*) (zipWith (-) limit' start') stride'))) :: [Int64]+ interior = map (\s -> max 0 (s - 1)) stride :: [Int64]+ zero <- bconstant xType 0.0+ dx <- bpad bar zero low high' interior xType+ accumulate cmap xVid dx+ Nothing -> return cmap+ where+ parseSliceAttrs :: [Attribute] -> ([Int64], [Int64], [Int64])+ parseSliceAttrs attrs =+ let s = findAttr "start_indices" attrs+ l = findAttr "limit_indices" attrs+ st = findAttr "strides" attrs+ in (s, l, st)++ findAttr :: Text -> [Attribute] -> [Int64]+ findAttr _ [] = error "autograd-hhlo: slice missing attribute"+ findAttr name (AttrRaw raw : rest) =+ let key = name <> " = array<i64:"+ in if key `T.isInfixOf` raw+ then parseIntList raw+ else findAttr name rest+ findAttr name (_ : rest) = findAttr name rest++ parseIntList :: Text -> [Int64]+ parseIntList raw =+ let inner = T.takeWhile (/= '>') $ T.dropWhile (/= '<') raw+ withoutLt = T.dropWhile (== '<') inner+ withoutPrefix = if "i64:" `T.isPrefixOf` withoutLt then T.drop 4 withoutLt else withoutLt+ nums = T.splitOn "," withoutPrefix+ in map ((fromIntegral :: Integer -> Int64) . read . T.unpack . T.strip) nums++-- ---------------------------------------------------------------------------+-- Concatenate VJP rule+-- ---------------------------------------------------------------------------++vjpConcatenate :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpConcatenate op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let inputVids = opOperands op+ inputTypes = opOperandTypes op+ dim <- case findConcatDim (opAttributes op) of+ Just d -> return d+ Nothing -> error "autograd-hhlo: concatenate missing dimension attribute"+ -- Split bar along concat dimension into slices matching each input+ let inputSizes = map (!! dim) (map ttShape inputTypes)+ splitAndAccumulate bar dim 0 inputVids inputTypes inputSizes cmap+ Nothing -> return cmap+ where+ findConcatDim :: [Attribute] -> Maybe Int+ findConcatDim [] = Nothing+ findConcatDim (AttrInt "dimension" d : _) = Just (fromIntegral d)+ findConcatDim (_ : rest) = findConcatDim rest++ splitAndAccumulate :: BTensor -> Int -> Integer -> [ValueId] -> [TensorType] -> [Integer] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+ splitAndAccumulate _ _ _ [] [] [] acc = return acc+ splitAndAccumulate bar dim offset (vid:vids) (itype:itypes) (sz:szs) acc = do+ let shape = ttShape itype+ start = replicate (length shape) (0 :: Integer)+ limit = zipWith (\i s -> if i == dim then offset + s else s) [0..] shape+ stride = replicate (length shape) (1 :: Integer)+ piece <- bslice bar (map fromIntegral start) (map fromIntegral limit) (map fromIntegral stride) itype+ acc' <- accumulate acc vid piece+ splitAndAccumulate bar dim (offset + sz) vids itypes szs acc'+ splitAndAccumulate _ _ _ _ _ _ _ = error "autograd-hhlo: concatenate operand mismatch"++-- ---------------------------------------------------------------------------+-- Pad VJP rule+-- ---------------------------------------------------------------------------++vjpPad :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpPad op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ (low, _high, interior) <- parsePadAttrs (opAttributes op)+ let xShape = ttShape xType+ stride = map (+ 1) interior :: [Int64]+ limit = zipWith3 (\l s sz -> l + sz * s) low stride (map fromIntegral xShape) :: [Int64]+ dx <- bslice bar low limit stride xType+ accumulate cmap xVid dx+ Nothing -> return cmap+ where+ parsePadAttrs :: [Attribute] -> Builder ([Int64], [Int64], [Int64])+ parsePadAttrs attrs =+ case (findAttr "edge_padding_low" attrs,+ findAttr "edge_padding_high" attrs,+ findAttr "interior_padding" attrs) of+ (Just l, Just h, Just i) -> return (l, h, i)+ _ -> error "autograd-hhlo: pad missing padding attributes"++ findAttr :: Text -> [Attribute] -> Maybe [Int64]+ findAttr _ [] = Nothing+ findAttr name (AttrRaw raw : rest) =+ let key = name <> " = array<i64:"+ in if key `T.isInfixOf` raw+ then Just (parseIntList raw)+ else findAttr name rest+ findAttr name (_ : rest) = findAttr name rest++ parseIntList :: Text -> [Int64]+ parseIntList raw =+ let inner = T.takeWhile (/= '>') $ T.dropWhile (/= '<') raw+ withoutLt = T.dropWhile (== '<') inner+ withoutPrefix = if "i64:" `T.isPrefixOf` withoutLt then T.drop 4 withoutLt else withoutLt+ nums = T.splitOn "," withoutPrefix+ in map ((fromIntegral :: Integer -> Int64) . read . T.unpack . T.strip) nums++-- ---------------------------------------------------------------------------+-- Gather / Scatter VJP rules (stubs)+-- ---------------------------------------------------------------------------++vjpGather :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpGather _ _ _ = error "autograd-hhlo: gather VJP not yet implemented"++vjpScatter :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpScatter _ _ _ = error "autograd-hhlo: scatter VJP not yet implemented"++-- ---------------------------------------------------------------------------+-- Convert VJP rule+-- ---------------------------------------------------------------------------++vjpConvert :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpConvert op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let xVid = opOperands op !! 0+ xType = opOperandTypes op !! 0+ dx <- bconvert bar xType+ accumulate cmap xVid dx+ Nothing -> return cmap++-- ---------------------------------------------------------------------------+-- Abs VJP rule+-- ---------------------------------------------------------------------------++vjpAbs :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpAbs op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ absX <- babs x+ dx <- bdivide x absX+ dx' <- bmultiply bar dx+ accumulate cmap (btVid x) dx'+ Nothing -> return cmap++-- ---------------------------------------------------------------------------+-- Maximum / Minimum VJP rules+-- ---------------------------------------------------------------------------++vjpMaximum :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpMaximum op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ y = operandBT op 1+ predGE <- bcompareGE x y+ zero <- bconstant (btType x) 0.0+ dx <- bselect predGE bar zero (btType x)+ dy <- bselect predGE zero bar (btType x)+ cmap' <- accumulate cmap (btVid x) dx+ accumulate cmap' (btVid y) dy+ Nothing -> return cmap++vjpMinimum :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpMinimum op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ y = operandBT op 1+ predLE <- bcompareGE y x+ zero <- bconstant (btType x) 0.0+ dx <- bselect predLE bar zero (btType x)+ dy <- bselect predLE zero bar (btType x)+ cmap' <- accumulate cmap (btVid x) dx+ accumulate cmap' (btVid y) dy+ Nothing -> return cmap++-- ---------------------------------------------------------------------------+-- Tanh VJP rule+-- ---------------------------------------------------------------------------++vjpTanh :: Operation -> [Maybe BTensor] -> Map ValueId BTensor -> Builder (Map ValueId BTensor)+vjpTanh op resultBars cmap = case getResultBar resultBars of+ Just bar -> do+ let x = operandBT op 0+ tanhX <- btanh x+ tanhSq <- bmultiply tanhX tanhX+ one <- bconstant (btType x) 1.0+ oneMinus <- bsubtract one tanhSq+ dx <- bmultiply bar oneMinus+ accumulate cmap (btVid x) dx+ Nothing -> return cmap
src/HHLO/EDSL/Ops.hs view
@@ -11,6 +11,7 @@ , divide , matmul , dotGeneral+ , einsum , linear , linearBatched -- * Unary element-wise ops@@ -40,9 +41,13 @@ , concatenate , concatenate2 , iota+ , split+ , stack -- * Reductions , reduceSum , reduceSumDim+ , productAll+ , productDim , reduceWindow , maxPool , avgPool@@ -94,6 +99,7 @@ , dynamicSlice , sort , convert+ , topK -- * Selection , select -- * Map@@ -131,7 +137,10 @@ import Prelude hiding (subtract, negate, maximum, minimum, abs, compare, map, tanh, sqrt, sin, cos, tan, floor, ceiling) +import Control.Monad (when) import Data.Int (Int64)+import Data.List (elemIndex)+import Data.Maybe (fromJust) import Data.Proxy import Data.Text (Text) import qualified Data.Text as T@@ -1879,3 +1888,200 @@ y1 <- reshape @'[] @'[1] y z1 <- reshape @'[] @'[1] z concatenate @'[1] @'[3] @d 0 [x1, y1, z1]++-- ---------------------------------------------------------------------------+-- Product reductions+-- ---------------------------------------------------------------------------++-- | Product of all elements (reduce over all dimensions).+productAll :: forall s d. (KnownShape s, KnownDType d) => Tensor s d -> Builder (Tensor '[] d)+productAll (Tensor x) = do+ let dims = [0 .. fromIntegral (length (shapeVal (Proxy @s))) - 1]+ productDim @s @'[] dims (Tensor x)++-- | Product elements over specific dimensions.+productDim :: forall sFrom sTo d.+ (KnownShape sFrom, KnownShape sTo, KnownDType d)+ => [Int] -> Tensor sFrom d -> Builder (Tensor sTo d)+productDim dims (Tensor x) = do+ let inType = tensorType (Proxy @sFrom) (Proxy @d)+ outType = tensorType (Proxy @sTo) (Proxy @d)+ elemType = tensorType (Proxy @'[]) (Proxy @d)+ -- Init value for stablehlo.reduce (must be scalar)+ oneVid <- emitOp "stablehlo.constant" [] []+ [AttrDenseElements [] (dtypeVal (Proxy @d)) [1.0]] elemType+ -- Build reduction region: two scalar args, apply stablehlo.multiply+ redBlock <- runBlockBuilder [elemType, elemType] $ do+ a <- arg @'[] @d+ b <- arg @'[] @d+ prodVid <- emitOp "stablehlo.multiply"+ [tensorValue a, tensorValue b]+ [elemType, elemType] [] elemType+ emitReturn [prodVid] [elemType]+ vid <- emitOpRegions "stablehlo.reduce"+ [x, oneVid]+ [inType, elemType]+ [AttrRaw $ "dimensions = array<i64: " <> T.intercalate ", " [T.pack (show d) | d <- dims] <> ">"]+ [Region [redBlock]]+ outType+ return (Tensor vid)++-- ---------------------------------------------------------------------------+-- Split and stack+-- ---------------------------------------------------------------------------++-- | Split a tensor into @n@ equal parts along dimension @dim@.+-- The size of @dim@ in the input must be evenly divisible by @n@.+split :: forall sIn sOut d.+ (KnownShape sIn, KnownShape sOut, KnownDType d)+ => Int64 -- ^ dimension to split along+ -> Int64 -- ^ number of equal splits+ -> Tensor sIn d+ -> Builder [Tensor sOut d]+split dim n t = do+ let sInShape = shapeVal (Proxy @sIn)+ rank = length sInShape+ dimSize = fromIntegral (sInShape !! fromIntegral dim) :: Int+ chunkSize = dimSize `div` fromIntegral n+ stride = replicate rank 1+ when (dimSize `mod` fromIntegral n /= 0) $+ error "split: dimension size not evenly divisible by number of splits"+ Prelude.mapM (\i -> do+ let start = [if j == fromIntegral dim then fromIntegral (i * chunkSize) else 0 | j <- [0..rank-1]]+ limit = [if j == fromIntegral dim then fromIntegral ((i+1) * chunkSize) else fromIntegral (sInShape !! j) | j <- [0..rank-1]]+ slice @sIn @sOut @d t start limit stride+ ) [0 .. fromIntegral n - 1]++-- | Stack a list of tensors along a new axis.+-- All inputs must have the same shape. The new axis is inserted at position+-- @dim@ in the output shape.+stack :: forall sIn sOut d.+ (KnownShape sIn, KnownShape sOut, KnownDType d)+ => Int64 -- ^ axis to insert and stack along+ -> [Tensor sIn d] -- ^ tensors to stack (must be non-empty)+ -> Builder (Tensor sOut d)+stack dim inputs = do+ let n = length inputs+ inShape = fmap fromIntegral (shapeVal (Proxy @sIn)) :: [Int64]+ outShape = fmap fromIntegral (shapeVal (Proxy @sOut)) :: [Int64]+ dt = dtypeVal (Proxy @d)+ rank = length inShape+ expectedOutShape = take (fromIntegral dim) inShape ++ [fromIntegral n] ++ drop (fromIntegral dim) inShape+ reshapedShape = take (fromIntegral dim) inShape ++ [1] ++ drop (fromIntegral dim) inShape+ inType = tensorType (Proxy @sIn) (Proxy @d)+ outType = tensorType (Proxy @sOut) (Proxy @d)+ reshapedType = TensorType (fmap fromIntegral reshapedShape) dt+ when (n == 0) $ error "stack: empty input list"+ when (outShape /= expectedOutShape) $+ error $ "stack: output shape mismatch. Expected " ++ show expectedOutShape ++ ", got " ++ show outShape+ reshapedVids <- Prelude.mapM (\(Tensor vid) ->+ emitOp "stablehlo.reshape" [vid] [inType] [] reshapedType+ ) inputs+ let dimAttr = AttrInt "dimension" (fromIntegral dim)+ vid <- emitOp "stablehlo.concatenate" reshapedVids (replicate n reshapedType) [dimAttr] outType+ return (Tensor vid)++-- ---------------------------------------------------------------------------+-- Top-K+-- ---------------------------------------------------------------------------++-- | Return the top-K values along a dimension, in descending order.+--+-- Note: A @topKWithIndices@ variant is future work; it requires multi-operand+-- sort support (sorting values and their index tensors together).+topK :: forall s sOut d.+ (KnownShape s, KnownShape sOut, KnownDType d)+ => Int64 -- ^ K+ -> Int64 -- ^ dimension to sort along+ -> Tensor s d+ -> Builder (Tensor sOut d)+topK k dim t = do+ let sShape = fmap fromIntegral (shapeVal (Proxy @s)) :: [Int64]+ outShape = fmap fromIntegral (shapeVal (Proxy @sOut)) :: [Int64]+ rank = length sShape+ expectedOutShape = [if j == fromIntegral dim then fromIntegral k else sShape !! j | j <- [0..rank-1]]+ when (outShape /= expectedOutShape) $+ error $ "topK: output shape mismatch. Expected " ++ show expectedOutShape ++ ", got " ++ show outShape+ -- Sort descending+ sorted <- sort @s @d t dim False $ \a b -> greaterThan a b+ -- Slice the first K elements along dim+ let start = replicate rank 0+ limit = [if j == fromIntegral dim then fromIntegral k else sShape !! j | j <- [0..rank-1]]+ stride = replicate rank 1+ slice @s @sOut @d sorted start limit stride++-- ---------------------------------------------------------------------------+-- Einsum+-- ---------------------------------------------------------------------------++-- | Einstein summation for two tensors.+--+-- Example: @einsum "ijk,jkl->il" a b@ contracts the @j@ and @k@ dimensions,+-- leaving @i@ from the left operand and @l@ from the right.+--+-- The caller must supply the output shape @sOut@ via type application or+-- inference context. Runtime validation ensures the subscript string agrees+-- with the type-level shapes.+einsum :: forall s1 s2 sOut d.+ (KnownShape s1, KnownShape s2, KnownShape sOut, KnownDType d)+ => String -- ^ subscript string, e.g. "ijk,jkl->il"+ -> Tensor s1 d+ -> Tensor s2 d+ -> Builder (Tensor sOut d)+einsum spec (Tensor x) (Tensor y) = do+ let (left, right, out) = parseEinsum spec+ s1Shape = shapeVal (Proxy @s1)+ s2Shape = shapeVal (Proxy @s2)+ sOutShape = shapeVal (Proxy @sOut)+ dt = dtypeVal (Proxy @d)+ rank1 = length s1Shape+ rank2 = length s2Shape+ rankOut = length sOutShape+ when (length left /= rank1) $ error "einsum: left subscript rank mismatch"+ when (length right /= rank2) $ error "einsum: right subscript rank mismatch"+ when (length out /= rankOut) $ error "einsum: output subscript rank mismatch"+ let batchLabels = [c | c <- left, c `elem` right, c `elem` out]+ contractLabels = [c | c <- left, c `elem` right, c `notElem` out]+ leftFree = [c | c <- left, c `elem` out, c `notElem` right]+ rightFree = [c | c <- right, c `elem` out, c `notElem` left]+ natural = batchLabels ++ leftFree ++ rightFree+ lhsBatch = fmap (fromIntegral . fromJust . (`elemIndex` left)) batchLabels+ rhsBatch = fmap (fromIntegral . fromJust . (`elemIndex` right)) batchLabels+ lhsContract = fmap (fromIntegral . fromJust . (`elemIndex` left)) contractLabels+ rhsContract = fmap (fromIntegral . fromJust . (`elemIndex` right)) contractLabels+ -- Build natural shape from actual dimensions+ lookupDim label+ | label `elem` left = s1Shape !! fromJust (label `elemIndex` left)+ | otherwise = s2Shape !! fromJust (label `elemIndex` right)+ naturalShape = fmap (fromIntegral . lookupDim) natural+ naturalType = TensorType (fmap fromIntegral naturalShape) dt+ inType1 = tensorType (Proxy @s1) (Proxy @d)+ inType2 = tensorType (Proxy @s2) (Proxy @d)+ outType = tensorType (Proxy @sOut) (Proxy @d)+ batchAttr = AttrString "batching_dims"+ ("[" <> T.intercalate ", " (fmap (T.pack . show) lhsBatch) <> "] x ["+ <> T.intercalate ", " (fmap (T.pack . show) rhsBatch) <> "]")+ contractingAttr = AttrString "contracting_dims"+ ("[" <> T.intercalate ", " (fmap (T.pack . show) lhsContract) <> "] x ["+ <> T.intercalate ", " (fmap (T.pack . show) rhsContract) <> "]")+ -- Validate output shape+ expectedOutShape = fmap (fromIntegral . lookupDim) out+ when (fmap fromIntegral sOutShape /= expectedOutShape) $+ error $ "einsum: output shape mismatch. Expected " ++ show expectedOutShape ++ ", got " ++ show (fmap fromIntegral sOutShape)+ vid <- emitOp "stablehlo.dot_general" [x, y] [inType1, inType2]+ [batchAttr, contractingAttr] naturalType+ if natural == out+ then return (Tensor vid)+ else do+ let perm = fmap (fromIntegral . fromJust . (`elemIndex` natural)) out+ permAttr = AttrIntList "permutation" perm+ vidT <- emitOp "stablehlo.transpose" [vid] [naturalType] [permAttr] outType+ return (Tensor vidT)+ where+ parseEinsum s =+ case break (== '-') s of+ (lhsRhs, '-':'>':outRest) ->+ case break (== ',') lhsRhs of+ (left, ',':right) -> (left, right, outRest)+ _ -> error "einsum: expected two operands separated by comma"+ _ -> error "einsum: expected -> in subscript string"
src/HHLO/IR/Pretty.hs view
@@ -176,10 +176,10 @@ <> (if null attrs then mempty else " " <> prettyAttrs attrs) <> " : () -> " <> prettyResults resultTypes pretty (Operation "stablehlo.sort" operands operandTypes attrs regions results resultTypes) =- -- Generic form (has regions; fallback would already use generic, but explicit is clearer).+ -- Generic form: regions must be wrapped in ( ) for PJRT parser compatibility. prettyResultVids results <> " = \"stablehlo.sort\"(" <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"- <> (if null regions then mempty else mconcat (map prettyRegion regions))+ <> (if null regions then mempty else " (" <> mconcat (intersperse (", ") (map prettyRegion regions)) <> ")") <> (if null attrs then mempty else " " <> prettyAttrs attrs) <> " : " <> prettyResultType operandTypes resultTypes pretty (Operation "stablehlo.return" operands operandTypes _ regions _ _) =
test/Main.hs view
@@ -5,7 +5,8 @@ import qualified Test.IR.Pretty as Pretty import qualified Test.IR.Builder as Builder import qualified Test.EDSL.Ops as EDSLOps-import qualified Test.ModuleBuilder as ModuleBuilder+import qualified Test.Autograd.Grad as AutogradGrad+import qualified Test.Autograd.Rules as AutogradRules import qualified Test.Runtime.EndToEnd as EndToEnd import qualified Test.Runtime.EndToEndArithmetic as Arith import qualified Test.Runtime.EndToEndShape as Shape@@ -15,6 +16,7 @@ import qualified Test.Runtime.EndToEndDataMovement as DataMovement import qualified Test.Runtime.EndToEndMultiValue as MultiValue import qualified Test.Runtime.EndToEndSession as Session+import qualified Test.Runtime.EndToEndAutograd as Autograd import qualified Test.Runtime.Buffer as Buffer import qualified Test.Runtime.Async as Async import qualified Test.Runtime.Errors as Errors@@ -38,6 +40,8 @@ [ Pretty.tests , Builder.tests , EDSLOps.tests+ , AutogradGrad.tests+ , AutogradRules.tests , EndToEnd.tests , Arith.tests , Shape.tests@@ -47,6 +51,7 @@ , DataMovement.tests , MultiValue.tests , Session.tests+ , Autograd.tests , Buffer.tests , Async.tests , Errors.tests
+ test/Test/Autograd/Grad.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE OverloadedStrings #-}++module Test.Autograd.Grad (tests) where++import Prelude hiding (sqrt)+import Test.Tasty+import Test.Tasty.HUnit++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Pretty+import HHLO.Autograd++import qualified Data.Text as T++tests :: TestTree+tests = testGroup "Autograd.Grad"+ [ testCase "sumOfSquaresGrad" $ do+ let f x = do sq <- multiply x x; sumAll sq+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "should contain stablehlo.multiply" ("stablehlo.multiply" `T.isInfixOf` text)+ assertBool "should contain stablehlo.add" ("stablehlo.add" `T.isInfixOf` text)+ , testCase "sumOfDoublesGrad" $ do+ let f x = do d <- add x x; sumAll d+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "should contain stablehlo.constant" ("stablehlo.constant" `T.isInfixOf` text)+ , testCase "multiplyChainGrad" $ do+ let f x = do+ sq <- multiply x x+ cb <- multiply sq x+ sumAll cb+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "should be non-empty" (not $ T.null text)+ , testCase "mixedOpsGrad" $ do+ let f x = do+ sq <- multiply x x+ s <- sqrt sq+ sumAll s+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "should contain stablehlo.sqrt" ("stablehlo.sqrt" `T.isInfixOf` text)+ ]
+ test/Test/Autograd/Rules.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE OverloadedStrings #-}++module Test.Autograd.Rules (tests) where++import Prelude hiding (negate)+import Test.Tasty+import Test.Tasty.HUnit++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Pretty+import HHLO.Autograd++import qualified Data.Text as T++tests :: TestTree+tests = testGroup "Autograd.Rules"+ [ testCase "vjpAdd" $ do+ let f x = do+ y <- add x x+ sumAll y+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "contains func.func" ("func.func" `T.isInfixOf` text)+ assertBool "contains return" ("return" `T.isInfixOf` text)+ , testCase "vjpMultiply" $ do+ let f x = do+ y <- multiply x x+ sumAll y+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "non-empty module" (not $ T.null text)+ , testCase "vjpNegate" $ do+ let f x = do+ y <- negate x+ sumAll y+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "non-empty module" (not $ T.null text)+ , testCase "vjpExponential" $ do+ let f x = do+ y <- exponential x+ sumAll y+ modu = gradModule @'[2] @'F32 f+ text = render modu+ assertBool "non-empty module" (not $ T.null text)+ ]
test/Test/EDSL/Ops.hs view
@@ -610,5 +610,76 @@ [ FuncArg "arg0" (TensorType [3] F32) ] $ do x <- arg @'[3] @'F32; y <- slice1 x 1; return y assertBool "stablehlo.slice" $ "stablehlo.slice" `T.isInfixOf` render modu+ , testCase "productAll" $ do+ let modu = moduleFromBuilder @'[] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32) ]+ $ do x <- arg @'[2, 3] @'F32; y <- productAll x; return y+ let rendered = render modu+ assertBool "stablehlo.reduce" $ "stablehlo.reduce" `T.isInfixOf` rendered+ assertBool "stablehlo.multiply" $ "stablehlo.multiply" `T.isInfixOf` rendered+ , testCase "productDim" $ do+ let modu = moduleFromBuilder @'[2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32) ]+ $ do x <- arg @'[2, 3] @'F32; y <- productDim @'[2, 3] @'[2] [1] x; return y+ let rendered = render modu+ assertBool "stablehlo.reduce" $ "stablehlo.reduce" `T.isInfixOf` rendered+ assertBool "stablehlo.multiply" $ "stablehlo.multiply" `T.isInfixOf` rendered+ , testCase "split" $ do+ let modu = moduleFromBuilder @'[2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [4] F32) ]+ $ do+ x <- arg @'[4] @'F32+ ys <- split @'[4] @'[2] 0 2 x+ case ys of+ (y1:_) -> return y1+ _ -> error "expected at least one split"+ assertBool "stablehlo.slice" $ "stablehlo.slice" `T.isInfixOf` render modu+ , testCase "stack" $ do+ let modu = moduleFromBuilder @'[2, 2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2] F32)+ , FuncArg "arg1" (TensorType [2] F32)+ ]+ $ do+ x <- arg @'[2] @'F32+ y <- arg @'[2] @'F32+ z <- stack @'[2] @'[2, 2] 0 [x, y]+ return z+ let rendered = render modu+ assertBool "stablehlo.reshape" $ "stablehlo.reshape" `T.isInfixOf` rendered+ assertBool "stablehlo.concatenate" $ "stablehlo.concatenate" `T.isInfixOf` rendered+ , testCase "topK" $ do+ let modu = moduleFromBuilder @'[2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [4] F32) ]+ $ do+ x <- arg @'[4] @'F32+ y <- topK @'[4] @'[2] 2 0 x+ return y+ let rendered = render modu+ assertBool "stablehlo.sort" $ "stablehlo.sort" `T.isInfixOf` rendered+ assertBool "stablehlo.slice" $ "stablehlo.slice" `T.isInfixOf` rendered+ , testCase "einsum matmul" $ do+ let modu = moduleFromBuilder @'[2, 2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32)+ , FuncArg "arg1" (TensorType [3, 2] F32)+ ]+ $ do+ x <- arg @'[2, 3] @'F32+ y <- arg @'[3, 2] @'F32+ z <- einsum "ij,jk->ik" x y+ return z+ assertBool "stablehlo.dot_general" $ "stablehlo.dot_general" `T.isInfixOf` render modu+ , testCase "einsum transpose output" $ do+ let modu = moduleFromBuilder @'[2, 2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32)+ , FuncArg "arg1" (TensorType [3, 2] F32)+ ]+ $ do+ x <- arg @'[2, 3] @'F32+ y <- arg @'[3, 2] @'F32+ z <- einsum "ij,jk->ki" x y+ return z+ let rendered = render modu+ assertBool "stablehlo.dot_general" $ "stablehlo.dot_general" `T.isInfixOf` rendered+ assertBool "stablehlo.transpose" $ "stablehlo.transpose" `T.isInfixOf` rendered ] ]
+ test/Test/Runtime/EndToEndAutograd.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Test.Runtime.EndToEndAutograd where++import qualified Data.Vector.Storable as V+import Test.Tasty+import Test.Tasty.HUnit++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Pretty+import HHLO.Autograd+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer+import Test.Utils++tests :: TestTree+tests = testGroup "EndToEnd.Autograd"+ [ testCase "grad sum of squares" $ withPJRTCPU $ \api client -> do+ let f x = do sq <- multiply x x; sumAll sq+ modu = gradModule @'[3] @'F32 f+ exec <- compile api client (render modu)+ let inp = V.fromList [1.0, 2.0, 3.0]+ bufIn <- toDeviceF32 api client inp [3]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 3+ -- grad = 2 * x = [2, 4, 6]+ let expected = V.fromList [2.0, 4.0, 6.0]+ assertBool "grad close" $+ V.and (V.zipWith (\r e -> abs (r - e) < 0.01) result expected)+ , testCase "grad sum of doubles" $ withPJRTCPU $ \api client -> do+ let f x = do d <- add x x; sumAll d+ modu = gradModule @'[3] @'F32 f+ exec <- compile api client (render modu)+ let inp = V.fromList [1.0, 2.0, 3.0]+ bufIn <- toDeviceF32 api client inp [3]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 3+ -- grad = [2, 2, 2]+ let expected = V.fromList [2.0, 2.0, 2.0]+ assertBool "grad close" $+ V.and (V.zipWith (\r e -> abs (r - e) < 0.01) result expected)+ , testCase "grad sum of exponentials" $ withPJRTCPU $ \api client -> do+ let f x = do e <- exponential x; sumAll e+ modu = gradModule @'[3] @'F32 f+ exec <- compile api client (render modu)+ let inp = V.fromList [0.0, 0.0, 0.0]+ bufIn <- toDeviceF32 api client inp [3]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 3+ -- grad = exp(x) = [1, 1, 1]+ let expected = V.fromList [1.0, 1.0, 1.0]+ assertBool "grad close" $+ V.and (V.zipWith (\r e -> abs (r - e) < 0.01) result expected)+ , testCase "grad matmul" $ withPJRTCPU $ \api client -> do+ let f x = do+ w <- constant @'[3, 2] @'F32 0.5+ y <- matmul x w+ sumAll y+ gradModu = gradModule @'[2, 3] @'F32 f+ exec <- compile api client (render gradModu)+ let inp = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]+ bufIn <- toDeviceF32 api client inp [2, 3]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 6+ -- grad = sum over cols of W = [0.5+0.5, 0.5+0.5, 0.5+0.5] for each row+ -- = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]+ let expected = V.fromList [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]+ assertBool "grad close" $+ V.and (V.zipWith (\r e -> abs (r - e) < 0.01) result expected)+ ]
test/Test/Runtime/EndToEndDataMovement.hs view
@@ -257,4 +257,18 @@ [bufOut] <- execute api exec [bufA] result <- fromDevice api bufOut 3 :: IO (V.Vector Word8) result @?= V.fromList [0, 1, 0]+ , testCase "topK" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [4] F32) ]+ $ do+ x <- arg @'[4] @'F32+ y <- topK @'[4] @'[2] 2 0 x+ return y+ exec <- compile api client (render modu)+ let inp = V.fromList [3.0, 1.0, 4.0, 1.0]+ bufIn <- toDeviceF32 api client inp [4]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 2+ -- Descending sort: [4.0, 3.0, 1.0, 1.0], top 2: [4.0, 3.0]+ result @?= V.fromList [4.0, 3.0] ]
test/Test/Runtime/EndToEndMatmul.hs view
@@ -96,4 +96,45 @@ let expected = V.fromList [3.0, 3.0, 7.5, 7.5] assertBool "dotGeneral close" $ all (\(r, e) -> abs (r - e) < 0.01) (zip (V.toList result) (V.toList expected))+ , testCase "einsum matmul" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[2, 2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32)+ , FuncArg "arg1" (TensorType [3, 2] F32)+ ]+ $ do+ x <- arg @'[2, 3] @'F32+ y <- arg @'[3, 2] @'F32+ z <- einsum "ij,jk->ik" x y+ return z+ exec <- compile api client (render modu)+ let a = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] :: V.Vector Float+ b = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] :: V.Vector Float+ bufA <- toDeviceF32 api client a [2, 3]+ bufB <- toDeviceF32 api client b [3, 2]+ [bufOut] <- execute api exec [bufA, bufB]+ result <- fromDeviceF32 api bufOut 4+ let expected = V.fromList [22.0, 28.0, 49.0, 64.0]+ assertBool "einsum matmul close" $+ all (\(r, e) -> abs (r - e) < 0.01) (zip (V.toList result) (V.toList expected))+ , testCase "einsum transpose output" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[2, 2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32)+ , FuncArg "arg1" (TensorType [3, 2] F32)+ ]+ $ do+ x <- arg @'[2, 3] @'F32+ y <- arg @'[3, 2] @'F32+ z <- einsum "ij,jk->ki" x y+ return z+ exec <- compile api client (render modu)+ let a = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] :: V.Vector Float+ b = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] :: V.Vector Float+ bufA <- toDeviceF32 api client a [2, 3]+ bufB <- toDeviceF32 api client b [3, 2]+ [bufOut] <- execute api exec [bufA, bufB]+ result <- fromDeviceF32 api bufOut 4+ -- Same as matmul but transposed: [[22,49],[28,64]] flattened row-major = [22,49,28,64]+ let expected = V.fromList [22.0, 49.0, 28.0, 64.0]+ assertBool "einsum transpose close" $+ all (\(r, e) -> abs (r - e) < 0.01) (zip (V.toList result) (V.toList expected)) ]
test/Test/Runtime/EndToEndReductions.hs view
@@ -66,4 +66,31 @@ let expected = V.fromList [3.5, 5.5, 11.5, 13.5] assertBool "avgPool close" $ V.and (V.zipWith (\r e -> abs (r - e) < 0.01) result expected)+ , testCase "productAll" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32) ]+ $ do+ x <- arg @'[2, 3] @'F32+ y <- productAll x+ return y+ exec <- compile api client (render modu)+ let inp = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]+ bufIn <- toDeviceF32 api client inp [2, 3]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 1+ result @?= V.fromList [720.0]+ , testCase "productDim" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2, 3] F32) ]+ $ do+ x <- arg @'[2, 3] @'F32+ y <- productDim @'[2, 3] @'[2] [1] x+ return y+ exec <- compile api client (render modu)+ let inp = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]+ bufIn <- toDeviceF32 api client inp [2, 3]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 2+ -- Row products: 1*2*3=6, 4*5*6=120+ result @?= V.fromList [6.0, 120.0] ]
test/Test/Runtime/EndToEndShape.hs view
@@ -93,4 +93,37 @@ [bufOut] <- execute api exec [] result <- fromDeviceF32 api bufOut 4 result @?= V.fromList [0.0, 1.0, 2.0, 3.0]+ , testCase "split" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [4] F32) ]+ $ do+ x <- arg @'[4] @'F32+ ys <- split @'[4] @'[2] 0 2 x+ case ys of+ (y1:_) -> return y1+ _ -> error "expected at least one split"+ exec <- compile api client (render modu)+ let inp = V.fromList [1.0, 2.0, 3.0, 4.0]+ bufIn <- toDeviceF32 api client inp [4]+ [bufOut] <- execute api exec [bufIn]+ result <- fromDeviceF32 api bufOut 2+ result @?= V.fromList [1.0, 2.0]+ , testCase "stack" $ withPJRTCPU $ \api client -> do+ let modu = moduleFromBuilder @'[2, 2] @'F32 "main"+ [ FuncArg "arg0" (TensorType [2] F32)+ , FuncArg "arg1" (TensorType [2] F32)+ ]+ $ do+ x <- arg @'[2] @'F32+ y <- arg @'[2] @'F32+ z <- stack @'[2] @'[2, 2] 0 [x, y]+ return z+ exec <- compile api client (render modu)+ let a = V.fromList [1.0, 2.0]+ b = V.fromList [3.0, 4.0]+ bufA <- toDeviceF32 api client a [2]+ bufB <- toDeviceF32 api client b [2]+ [bufOut] <- execute api exec [bufA, bufB]+ result <- fromDeviceF32 api bufOut 4+ result @?= V.fromList [1.0, 2.0, 3.0, 4.0] ]