packages feed

hhlo-0.1.0.0: test/Test/Runtime/EndToEndNN.hs

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

module Test.Runtime.EndToEndNN 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.AST (FuncArg(..), TensorType(..))
import HHLO.IR.Builder
import HHLO.IR.Pretty
import HHLO.Runtime.Compile
import HHLO.Runtime.Execute
import HHLO.Runtime.Buffer
import Test.Utils

tests :: TestTree
tests = testGroup "EndToEnd.NN"
    [ testCase "conv2d identity" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[1, 2, 2, 1] @'F32 "main"
                [ FuncArg "arg0" (TensorType [1, 4, 4, 1] F32) ]
                $ do
                    x <- arg @'[1, 4, 4, 1] @'F32
                    k <- constant @'[3, 3, 1, 1] @'F32 0.0
                    y <- conv2d x k
                    return y
        exec <- compile api client (render modu)
        let inp = V.fromList [1..16]
        bufIn <- toDeviceF32 api client inp [1, 4, 4, 1]
        [bufOut] <- execute api exec [bufIn]
        result <- fromDeviceF32 api bufOut 4
        -- With zero kernel, output is all zeros
        V.all (== 0.0) result @? "conv2d with zero kernel should output zeros"
    , testCase "softmax1D" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[3] @'F32 "main"
                [ FuncArg "arg0" (TensorType [3] F32) ]
                $ do
                    x <- arg
                    y <- softmax1D x
                    return y
        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
        -- softmax of [0,0,0] = [1/3, 1/3, 1/3]
        let expected = V.fromList [1/3, 1/3, 1/3]
        assertBool "softmax sums to 1" $ abs (V.sum result - 1.0) < 0.01
        assertBool "softmax values close" $
            V.and (V.zipWith (\r e -> abs (r - e) < 0.01) result expected)
    , testCase "softmax2D" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[2, 3] @'F32 "main"
                [ FuncArg "arg0" (TensorType [2, 3] F32) ]
                $ do
                    x <- arg
                    y <- softmax2D x
                    return y
        exec <- compile api client (render modu)
        let inp = V.fromList [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
        bufIn <- toDeviceF32 api client inp [2, 3]
        [bufOut] <- execute api exec [bufIn]
        result <- fromDeviceF32 api bufOut 6
        assertBool "softmax2D row 0 sums to 1" $ abs (V.sum (V.slice 0 3 result) - 1.0) < 0.01
        assertBool "softmax2D row 1 sums to 1" $ abs (V.sum (V.slice 3 3 result) - 1.0) < 0.01
    , testCase "batchNorm identity" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[1, 2, 2, 2] @'F32 "main"
                [ FuncArg "arg0" (TensorType [1, 2, 2, 2] F32) ]
                $ do
                    x <- arg @'[1, 2, 2, 2] @'F32
                    s <- constant @'[2] @'F32 1.0
                    o <- constant @'[2] @'F32 0.0
                    m <- constant @'[2] @'F32 0.0
                    v <- constant @'[2] @'F32 1.0
                    y <- batchNormInference x s o m v
                    return y
        exec <- compile api client (render modu)
        let inp = V.fromList [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
        bufIn <- toDeviceF32 api client inp [1, 2, 2, 2]
        [bufOut] <- execute api exec [bufIn]
        result <- fromDeviceF32 api bufOut 8
        -- With identity params, output should equal input
        assertBool "batchNorm identity" $
            all (\(r, e) -> abs (r - e) < 0.01) (zip (V.toList result) (V.toList inp))
    , testCase "gelu" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[3] @'F32 "main"
                [ FuncArg "arg0" (TensorType [3] F32) ]
                $ do
                    x <- arg @'[3] @'F32
                    y <- gelu x
                    return y
        exec <- compile api client (render modu)
        let inp = V.fromList [0.0, 1.0, -1.0]
        bufIn <- toDeviceF32 api client inp [3]
        [bufOut] <- execute api exec [bufIn]
        result <- fromDeviceF32 api bufOut 3
        -- gelu(0) ≈ 0, gelu(1) ≈ 0.841, gelu(-1) ≈ -0.159
        assertBool "gelu(0) ≈ 0" $ abs (result V.! 0) < 0.01
        assertBool "gelu(1) ≈ 0.84" $ abs (result V.! 1 - 0.841) < 0.01
        assertBool "gelu(-1) ≈ -0.16" $ abs (result V.! 2 + 0.159) < 0.05
    , testCase "layerNorm" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[1, 2, 4] @'F32 "main"
                [ FuncArg "arg0" (TensorType [1, 2, 4] F32) ]
                $ do
                    x <- arg @'[1, 2, 4] @'F32
                    g <- constant @'[4] @'F32 1.0
                    b <- constant @'[4] @'F32 0.0
                    y <- layerNorm x g b
                    return y
        exec <- compile api client (render modu)
        let inp = V.fromList [1.0, 2.0, 3.0, 4.0, 1.0, 1.0, 1.0, 1.0]
        bufIn <- toDeviceF32 api client inp [1, 2, 4]
        [bufOut] <- execute api exec [bufIn]
        result <- fromDeviceF32 api bufOut 8
        -- Row 1 is all ones, so layerNorm should give all ~0 (after normalization)
        let row1 = V.slice 4 4 result
        assertBool "layerNorm of uniform row has near-zero mean" $
            abs (V.sum row1 / 4) < 0.1
    , testCase "globalAvgPool" $ withPJRTCPU $ \api client -> do
        let modu = moduleFromBuilder @'[1, 2] @'F32 "main"
                [ FuncArg "arg0" (TensorType [1, 4, 4, 2] F32) ]
                $ do
                    x <- arg @'[1, 4, 4, 2] @'F32
                    y <- globalAvgPool x
                    return y
        exec <- compile api client (render modu)
        -- [1,4,4,2] layout: c varies fastest, then w, then h.
        -- Channel 0 (even offsets) = 1.0, channel 1 (odd offsets) = 2.0
        let inp = V.fromList [if even i then 1.0 else 2.0 | i <- [0..31]]
        bufIn <- toDeviceF32 api client inp [1, 4, 4, 2]
        [bufOut] <- execute api exec [bufIn]
        result <- fromDeviceF32 api bufOut 2
        -- Average of 16 ones = 1.0, average of 16 twos = 2.0
        assertBool ("globalAvgPool channel 0: " ++ show (V.toList result)) $ abs (result V.! 0 - 1.0) < 0.01
        assertBool ("globalAvgPool channel 1: " ++ show (V.toList result)) $ abs (result V.! 1 - 2.0) < 0.01
    ]