{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NondecreasingIndentation #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE AllowAmbiguousTypes #-} module Main where import Test.Tasty (TestTree, testGroup, defaultMain) import Test.Tasty.HUnit (Assertion, HasCallStack, testCase, assertBool, assertEqual, (@?=)) import Test.Tasty.QuickCheck (testProperty) import Test.QuickCheck (property, conjoin, choose, suchThat, forAll, sublistOf, label, classify, whenFail, counterexample, elements, vectorOf, Gen, Testable, frequency, sized, Property, arbitrary, Arbitrary, listOf) import Test.QuickCheck.Monadic (forAllM, monadicIO, run, assert) import Control.Monad (when) import Data.Functor.Foldable (Fix(..), Recursive(..), Corecursive(..)) import Data.Maybe (fromMaybe) import Data.Text (isInfixOf) import Data.Monoid ((<>)) import System.Directory (getTemporaryDirectory) import System.IO.Temp (withTempFile) import Prelude import HTVM.Prelude import HTVM genTensorList1 :: (Arbitrary e) => Gen [e] genTensorList1 = do x <- choose (0,10) vectorOf x $ arbitrary genTensorList2 :: (Arbitrary e) => Gen [[e]] genTensorList2 = do x <- choose (0,10) y <- choose (0,10) vectorOf x $ vectorOf y $ arbitrary genShape :: Gen [Integer] genShape = do ndim <- choose (0,4) vectorOf ndim (choose (0,5)) withTestModule :: Stmt Function -> (ModuleLib -> IO b) -> IO b withTestModule mf act = withTmpf "htvm-test-module" $ \fp -> do {- traceM $ "file: " <> fp -} act =<< do buildModule defaultConfig fp $ stageModule $ do f <- mf modul [f] class EpsilonEqual a where epsilonEqual :: Rational -> a -> a -> Bool instance EpsilonEqual Float where epsilonEqual eps a b = abs (a - b) < fromRational eps instance EpsilonEqual Double where epsilonEqual eps a b = abs (a - b) < fromRational eps instance EpsilonEqual a => EpsilonEqual [a] where epsilonEqual eps a b = length a == length b && (all (uncurry (epsilonEqual eps)) (a`zip`b)) assertEpsilonEqual :: (EpsilonEqual a, HasCallStack) => String -> Rational -> a -> a -> Assertion assertEpsilonEqual msg eps a b = assertBool msg (epsilonEqual eps a b) main :: IO () main = defaultMain $ testGroup "All" $ reverse [ testGroup "Uninitialized Tensor FFI should work" $ let go :: forall e . TVMElemType e => [Integer] -> IO () go sh = do a <- newEmptyTensor @e sh KDLCPU 0 assertEqual "poke-peek-2" (tensorNDim a) (ilength sh) assertEqual "poke-peek-1" (tensorShape a) sh gen :: forall e . TVMElemType e => Property gen = forAll genShape $ monadicIO . run . go @e in [ testProperty "Int32" $ (gen @Int32) , testProperty "Word32" $ (gen @Word32) , testProperty "Float" $ (gen @Float) , testProperty "Int64" $ (gen @Int64) , testProperty "Word64" $ (gen @Word64) , testProperty "Double" $ (gen @Double) ] , testGroup "Initiallized Tensor FFI should work" $ let go :: forall d i e . (TVMData d i e, Eq e, Eq d, Show d) => d -> IO () go l = do a <- newTensor l KDLCPU 0 assertEqual "poke-peek-1" (tensorNDim a) (tvmDataNDim l) assertEqual "poke-peek-2" (tensorShape a) (tvmDataShape l) l2 <- peekTensor a assertEqual "poke-peek-3" l l2 return () flatzero :: [[e]] -> [[e]] flatzero x | length (concat x) == 0 = [] | otherwise = x gen1 :: forall e i . (Eq e, Show e, TVMData [e] i e, Arbitrary e) => Property gen1 = forAll (genTensorList1 @e) $ monadicIO . run . go gen2 :: forall e i . (Eq e, Show e, TVMData [[e]] i e, Arbitrary e) => Property gen2 = forAll (genTensorList2 @e) $ monadicIO . run . go . flatzero in [ testProperty "[Int32]" $ (gen1 @Int32) , testProperty "[Word32]" $ (gen1 @Word32) , testProperty "[Float]" $ (gen1 @Float) , testProperty "[Int64]" $ (gen1 @Int64) , testProperty "[Word64]" $ (gen1 @Word64) , testProperty "[Double]" $ (gen1 @Double) , testProperty "[[Int32]]" $ (gen2 @Int32) , testProperty "[[Word32]]" $ (gen2 @Word32) , testProperty "[[Float]]" $ (gen2 @Float) , testProperty "[[Int64]]" $ (gen2 @Int64) , testProperty "[[Word64]]" $ (gen2 @Word64) , testProperty "[[Double]]" $ (gen2 @Double) ] , testGroup "Copy FFI should work for tensors" $ let go :: forall d i e . (TVMData d i e, Eq e, Eq d, Show d) => d -> IO () go l = do src <- newTensor l KDLCPU 0 dst <- newEmptyTensor @e (tensorShape src) KDLCPU 0 tensorCopy dst src l2 <- peekTensor dst assertEqual "copy-peek-1" l l2 return () flatzero :: [[e]] -> [[e]] flatzero x | length (concat x) == 0 = [] | otherwise = x gen1 :: forall e i . (Eq e, Show e, TVMData [e] i e, Arbitrary e) => Property gen1 = forAll (genTensorList1 @e) $ monadicIO . run . go gen2 :: forall e i . (Eq e, Show e, TVMData [[e]] i e, Arbitrary e) => Property gen2 = forAll (genTensorList2 @e) $ monadicIO . run . go . flatzero in [ testProperty "[Int32]" $ (gen1 @Int32) , testProperty "[Word32]" $ (gen1 @Word32) , testProperty "[Float]" $ (gen1 @Float) , testProperty "[Int64]" $ (gen1 @Int64) , testProperty "[Word64]" $ (gen1 @Word64) , testProperty "[Double]" $ (gen1 @Double) , testProperty "[[Int32]]" $ (gen2 @Int32) , testProperty "[[Word32]]" $ (gen2 @Word32) , testProperty "[[Float]]" $ (gen2 @Float) , testProperty "[[Int64]]" $ (gen2 @Int64) , testProperty "[[Word64]]" $ (gen2 @Word64) , testProperty "[[Double]]" $ (gen2 @Double) ] , testCase "Compiler (g++ -ltvm) should be available" $ do withTmpf "htvm-compiler-test" $ \x -> do _ <- compileModuleGen defaultConfig x (ModuleGenSrc undefined "int main() { return 0; }") return () , testCase "Pretty-printer (clang-format) should be available" $ do _ <- prettyCpp "int main() { return 0; }" return () , testCase "Function printer should work" $ do dump <- printFunction defaultConfig =<< do stageFunction $ do s <- shapevar [10] function "vecadd" [("A",float32,s),("B",float32,s)] $ \[a,b] -> do compute s $ \e -> a![e!0] + b![e!0] assertBool "dump should contain 'produce' keyword" $ isInfixOf "produce" dump , testCase "Simple model should work" $ let dim0 = 4 :: Integer fname = "vecadd" in do withTestModule (do s <- shapevar [fromInteger dim0] function fname [("A",float32,s),("B",float32,s)] $ \[a,b] -> do compute s $ \e -> a![e!0] + b![e!0] ) $ \(ModuleLib p _) -> do withModule p $ \hmod -> do withFunction fname hmod $ \fmod -> do a <- newTensor @[Float] [1,2,3,4] KDLCPU 0 b <- newTensor @[Float] [10,20,30,40] KDLCPU 0 c <- newEmptyTensor @Float [dim0] KDLCPU 0 callTensorFunction c fmod [a,b] assertEqual "Simple model result" [11,22,33,44::Float] =<< peekTensor c , testCase "Reduce axis operation should compile" $ withTestModule (do s <- shapevar [4] function "reduce" [("A",float32,s)] $ \[a] -> do IterVar r <- reduce_axis (0,3) compute ShapeScalar $ \_ -> esum (a![r], [r]) ) $ \_ -> return () , testCase "Conv2d operation should compile" $ withTestModule (do sa <- shapevar [1,1,10,10] sk <- shapevar [1,1,3,3] function "reduce" [("A",float32,sa), ("k",float32,sk)] $ \[a,k] -> do return $ conv2d_nchw a k def ) $ \_ -> return () , testCase "Pad operation should compile" $ withTestModule (do sa <- shapevar [1,1,10,10] function "reduce" [("A",float32,sa) ] $ \[a] -> do return $ pad a def{pad_value=33, pad_before=[2,2,2,2]} ) $ \_ -> return () , testCase "Parallel schedule should compile" $ withTestModule (do sa <- shapevar [1,1,10,10] function "reduce" [("A",float32,sa) ] $ \[a] -> do c <- assign $ pad a def{pad_value=33, pad_before=[2,2,2,2]} r <- axisId c 0 s <- schedule [c] parallel s c r return c ) $ \_ -> return () , testCase "Sigmoid primitive should work" $ withTestModule (do s <- shapevar [4] function "sigmoid" [("A",float32,s)] $ \[a] -> do c <- assign $ sigmoid a return c ) $ \(ModuleLib p _) -> do withModule p $ \hmod -> do withFunction "sigmoid" hmod $ \fmod -> let inp = [1,2,3,4] :: [Float] out = map (\x -> 1.0 / (1.0 + exp (- x))) inp in do a <- newTensor @[Float] [1,2,3,4] KDLCPU 0 c <- newEmptyTensor @Float [4] KDLCPU 0 callTensorFunction c fmod [a] c_ <- peekTensor c assertEpsilonEqual "Simple model result" 1e-5 out c_ , testCase "Split primitive should compile" $ withTestModule (do sa <- shapevar [2,4] function "reduce" [("A",float32,sa) ] $ \[a] -> do c <- assign $ split a [1] 0 return (c!0) ) $ \_ -> return () ]