packages feed

hhlo-0.1.0.0: examples/26-unet.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

-- | Example 26: UNet for image segmentation.
--
-- Toy-sized UNet for fast CPU execution:
--   Input: 16x16x1  →  Output: 16x16x2 (2-class segmentation)
--   2 encoder/decoder stages with skip connections
--
-- Build and run with:
--   LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-unet

module Main where

import qualified Data.Text as T
import qualified Data.Vector.Storable as V
import Foreign.C
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr
import Foreign.Storable (peek)

import HHLO.Core.Types
import HHLO.EDSL.Ops
import HHLO.IR.AST (FuncArg(..), TensorType(..))
import HHLO.IR.Builder
import HHLO.IR.Pretty
import HHLO.Runtime.PJRT.FFI
import HHLO.Runtime.PJRT.Types
import HHLO.Runtime.PJRT.Error
import HHLO.Runtime.Compile
import HHLO.Runtime.Buffer
import HHLO.Runtime.Execute

main :: IO ()
main = do
    putStrLn "=== Example 26: UNet (toy, 16x16 input) ==="

    api <- withCString "deps/pjrt/libpjrt_cpu.so" $ \path -> do
        alloca $ \apiPtrPtr -> do
            checkError nullPtr $ c_pjrtLoadPlugin path apiPtrPtr
            PJRTApi <$> peek apiPtrPtr

    client <- alloca $ \clientPtrPtr -> do
        checkError (unApi api) $ c_pjrtCreateClient (unApi api) clientPtrPtr
        PJRTClient <$> peek clientPtrPtr

    let modu = moduleFromBuilder @'[1, 16, 16, 2] @'F32 "main"
            [ FuncArg "input" (TensorType [1, 16, 16, 1] F32)
            ]
            $ do
                x <- arg @'[1, 16, 16, 1] @'F32

                -- ========== Encoder ==========

                -- Encoder stage 1: 1 -> 16 channels
                wE1a <- constant @'[3, 3, 1, 16] @'F32 0.01
                e1 <- conv2dWithPadding @1 @16 @16 @1 @16 @3 @3 @16 @16 [1, 1] [[1, 1], [1, 1]] x wE1a
                e1 <- relu e1
                wE1b <- constant @'[3, 3, 16, 16] @'F32 0.01
                e1 <- conv2dWithPadding @1 @16 @16 @16 @16 @3 @3 @16 @16 [1, 1] [[1, 1], [1, 1]] e1 wE1b
                e1 <- relu e1
                -- Skip1 = e1 (shape [1,16,16,16])

                -- Downsample: maxPool 2x2/2
                d1 <- maxPool [2, 2] [2, 2] [[0, 0], [0, 0]] e1

                -- Encoder stage 2: 16 -> 32 channels
                wE2a <- constant @'[3, 3, 16, 32] @'F32 0.01
                e2 <- conv2dWithPadding @1 @8 @8 @16 @32 @3 @3 @8 @8 [1, 1] [[1, 1], [1, 1]] d1 wE2a
                e2 <- relu e2
                wE2b <- constant @'[3, 3, 32, 32] @'F32 0.01
                e2 <- conv2dWithPadding @1 @8 @8 @32 @32 @3 @3 @8 @8 [1, 1] [[1, 1], [1, 1]] e2 wE2b
                e2 <- relu e2
                -- Skip2 = e2 (shape [1,8,8,32])

                -- Downsample: maxPool 2x2/2
                d2 <- maxPool [2, 2] [2, 2] [[0, 0], [0, 0]] e2

                -- ========== Bottleneck ==========
                wBa <- constant @'[3, 3, 32, 64] @'F32 0.01
                b <- conv2dWithPadding @1 @4 @4 @32 @64 @3 @3 @4 @4 [1, 1] [[1, 1], [1, 1]] d2 wBa
                b <- relu b
                wBb <- constant @'[3, 3, 64, 64] @'F32 0.01
                b <- conv2dWithPadding @1 @4 @4 @64 @64 @3 @3 @4 @4 [1, 1] [[1, 1], [1, 1]] b wBb
                b <- relu b

                -- ========== Decoder ==========

                -- Decoder stage 2: upsample 4x4 -> 8x8, concat with skip2
                -- Transpose conv: [1,4,4,64] -> [1,8,8,32]
                wD2up <- constant @'[2, 2, 32, 64] @'F32 0.01
                u2 <- transposeConvolution [1, 2, 2, 1] [[1, 1], [1, 1]] b wD2up
                -- Concatenate with skip2 along channel dim (axis 3)
                u2 <- concatenate2 @'[1, 8, 8, 32] @'[1, 8, 8, 32] @'[1, 8, 8, 64] @'F32 3 u2 e2
                wD2a <- constant @'[3, 3, 64, 32] @'F32 0.01
                u2 <- conv2dWithPadding @1 @8 @8 @64 @32 @3 @3 @8 @8 [1, 1] [[1, 1], [1, 1]] u2 wD2a
                u2 <- relu u2
                wD2b <- constant @'[3, 3, 32, 32] @'F32 0.01
                u2 <- conv2dWithPadding @1 @8 @8 @32 @32 @3 @3 @8 @8 [1, 1] [[1, 1], [1, 1]] u2 wD2b
                u2 <- relu u2

                -- Decoder stage 1: upsample 8x8 -> 16x16, concat with skip1
                wD1up <- constant @'[2, 2, 16, 32] @'F32 0.01
                u1 <- transposeConvolution [1, 2, 2, 1] [[1, 1], [1, 1]] u2 wD1up
                -- Concatenate with skip1 along channel dim (axis 3)
                u1 <- concatenate2 @'[1, 16, 16, 16] @'[1, 16, 16, 16] @'[1, 16, 16, 32] @'F32 3 u1 e1
                wD1a <- constant @'[3, 3, 32, 16] @'F32 0.01
                u1 <- conv2dWithPadding @1 @16 @16 @32 @16 @3 @3 @16 @16 [1, 1] [[1, 1], [1, 1]] u1 wD1a
                u1 <- relu u1
                wD1b <- constant @'[3, 3, 16, 16] @'F32 0.01
                u1 <- conv2dWithPadding @1 @16 @16 @16 @16 @3 @3 @16 @16 [1, 1] [[1, 1], [1, 1]] u1 wD1b
                u1 <- relu u1

                -- Final 1x1 conv: 16 -> 2 channels
                wOut <- constant @'[1, 1, 16, 2] @'F32 0.01
                conv2dWithPadding @1 @16 @16 @16 @2 @1 @1 @16 @16 [1, 1] [[0, 0], [0, 0]] u1 wOut

    putStrLn "Generated MLIR (first 20 lines):"
    let lines_ = T.lines (render modu)
    mapM_ (putStrLn . T.unpack) (take 20 lines_)
    putStrLn "  ..."

    exec <- compile api client (render modu)

    let input = V.fromList [0.5 | _ <- [1..256]] :: V.Vector Float
    buf <- toDeviceF32 api client input [1, 16, 16, 1]

    [bufY] <- execute api exec [buf]
    result <- fromDeviceF32 api bufY 512

    putStrLn $ "Output shape: [1, 16, 16, 2]"
    putStrLn $ "Output (first 8): " ++ show (take 8 (V.toList result))
    putStrLn $ "Output (last 8):  " ++ show (drop 504 (V.toList result))
    putStrLn "✓ PASS (executed successfully)"

    checkError (unApi api) $ c_pjrtClientDestroy (unApi api) (unClient client)
  where
    unApi (PJRTApi p) = p
    unClient (PJRTClient p) = p