packages feed

futhark-0.17.1: src/Futhark/CodeGen/ImpGen/Kernels/Transpose.hs

-- | Carefully optimised implementations of GPU transpositions.
-- Written in ImpCode so we can compile it to both CUDA and OpenCL.
module Futhark.CodeGen.ImpGen.Kernels.Transpose
  ( TransposeType (..),
    TransposeArgs,
    mapTransposeKernel,
  )
where

import Futhark.CodeGen.ImpCode.Kernels
import Futhark.IR.Prop.Types
import Futhark.Util.IntegralExp (divUp, quot, rem)
import Prelude hiding (quot, rem)

-- | Which form of transposition to generate code for.
data TransposeType
  = TransposeNormal
  | TransposeLowWidth
  | TransposeLowHeight
  | -- | For small arrays that do not
    -- benefit from coalescing.
    TransposeSmall
  deriving (Eq, Ord, Show)

-- | The types of the arguments accepted by a transposition function.
type TransposeArgs =
  ( VName,
    TExp Int32,
    VName,
    TExp Int32,
    TExp Int32,
    TExp Int32,
    TExp Int32,
    TExp Int32,
    TExp Int32,
    VName
  )

elemsPerThread :: TExp Int32
elemsPerThread = 4

mapTranspose :: TExp Int32 -> TransposeArgs -> PrimType -> TransposeType -> KernelCode
mapTranspose block_dim args t kind =
  case kind of
    TransposeSmall ->
      mconcat
        [ get_ids,
          dec our_array_offset $ vi32 get_global_id_0 `quot` (height * width) * (height * width),
          dec x_index $ (vi32 get_global_id_0 `rem` (height * width)) `quot` height,
          dec y_index $ vi32 get_global_id_0 `rem` height,
          dec odata_offset $
            (basic_odata_offset `quot` primByteSize t) + vi32 our_array_offset,
          dec idata_offset $
            (basic_idata_offset `quot` primByteSize t) + vi32 our_array_offset,
          dec index_in $ vi32 y_index * width + vi32 x_index,
          dec index_out $ vi32 x_index * height + vi32 y_index,
          when
            (vi32 get_global_id_0 .<. width * height * num_arrays)
            ( Write odata (elements $ sExt64 $ vi32 odata_offset + vi32 index_out) t (Space "global") Nonvolatile $
                index idata (elements $ sExt64 $ vi32 idata_offset + vi32 index_in) t (Space "global") Nonvolatile
            )
        ]
    TransposeLowWidth ->
      mkTranspose $
        lowDimBody
          (vi32 get_group_id_0 * block_dim + (vi32 get_local_id_0 `quot` muly))
          ( vi32 get_group_id_1 * block_dim * muly + vi32 get_local_id_1
              + (vi32 get_local_id_0 `rem` muly) * block_dim
          )
          ( vi32 get_group_id_1 * block_dim * muly + vi32 get_local_id_0
              + (vi32 get_local_id_1 `rem` muly) * block_dim
          )
          (vi32 get_group_id_0 * block_dim + (vi32 get_local_id_1 `quot` muly))
    TransposeLowHeight ->
      mkTranspose $
        lowDimBody
          ( vi32 get_group_id_0 * block_dim * mulx + vi32 get_local_id_0
              + (vi32 get_local_id_1 `rem` mulx) * block_dim
          )
          (vi32 get_group_id_1 * block_dim + (vi32 get_local_id_1 `quot` mulx))
          (vi32 get_group_id_1 * block_dim + (vi32 get_local_id_0 `quot` mulx))
          ( vi32 get_group_id_0 * block_dim * mulx + vi32 get_local_id_1
              + (vi32 get_local_id_0 `rem` mulx) * block_dim
          )
    TransposeNormal ->
      mkTranspose $
        mconcat
          [ dec x_index $ vi32 get_global_id_0,
            dec y_index $ vi32 get_group_id_1 * tile_dim + vi32 get_local_id_1,
            when (vi32 x_index .<. width) $
              For j (untyped elemsPerThread) $
                let i = vi32 j * (tile_dim `quot` elemsPerThread)
                 in mconcat
                      [ dec index_in $ (vi32 y_index + i) * width + vi32 x_index,
                        when (vi32 y_index + i .<. height) $
                          Write
                            block
                            ( elements $
                                sExt64 $
                                  (vi32 get_local_id_1 + i) * (tile_dim + 1)
                                    + vi32 get_local_id_0
                            )
                            t
                            (Space "local")
                            Nonvolatile
                            $ index
                              idata
                              (elements $ sExt64 $ vi32 idata_offset + vi32 index_in)
                              t
                              (Space "global")
                              Nonvolatile
                      ],
            Op $ Barrier FenceLocal,
            SetScalar x_index $ untyped $ vi32 get_group_id_1 * tile_dim + vi32 get_local_id_0,
            SetScalar y_index $ untyped $ vi32 get_group_id_0 * tile_dim + vi32 get_local_id_1,
            when (vi32 x_index .<. height) $
              For j (untyped elemsPerThread) $
                let i = vi32 j * (tile_dim `quot` elemsPerThread)
                 in mconcat
                      [ dec index_out $ (vi32 y_index + i) * height + vi32 x_index,
                        when (vi32 y_index + i .<. width) $
                          Write
                            odata
                            (elements $ sExt64 $ vi32 odata_offset + vi32 index_out)
                            t
                            (Space "global")
                            Nonvolatile
                            $ index
                              block
                              ( elements $
                                  sExt64 $
                                    vi32 get_local_id_0 * (tile_dim + 1) + vi32 get_local_id_1 + i
                              )
                              t
                              (Space "local")
                              Nonvolatile
                      ]
          ]
  where
    dec v (TPrimExp e) =
      DeclareScalar v Nonvolatile (primExpType e) <> SetScalar v e
    tile_dim = 2 * block_dim

    when a b = If a b mempty

    ( odata,
      basic_odata_offset,
      idata,
      basic_idata_offset,
      width,
      height,
      mulx,
      muly,
      num_arrays,
      block
      ) = args

    -- Be extremely careful when editing this list to ensure that
    -- the names match up.  Also, be careful that the tags on
    -- these names do not conflict with the tags of the
    -- surrounding code.  We accomplish the latter by using very
    -- low tags (normal variables start at least in the low
    -- hundreds).
    [ our_array_offset,
      x_index,
      y_index,
      odata_offset,
      idata_offset,
      index_in,
      index_out,
      get_global_id_0,
      get_local_id_0,
      get_local_id_1,
      get_group_id_0,
      get_group_id_1,
      get_group_id_2,
      j
      ] =
        zipWith (flip VName) [30 ..] $
          map
            nameFromString
            [ "our_array_offset",
              "x_index",
              "y_index",
              "odata_offset",
              "idata_offset",
              "index_in",
              "index_out",
              "get_global_id_0",
              "get_local_id_0",
              "get_local_id_1",
              "get_group_id_0",
              "get_group_id_1",
              "get_group_id_2",
              "j"
            ]

    get_ids =
      mconcat
        [ DeclareScalar get_global_id_0 Nonvolatile int32,
          Op $ GetGlobalId get_global_id_0 0,
          DeclareScalar get_local_id_0 Nonvolatile int32,
          Op $ GetLocalId get_local_id_0 0,
          DeclareScalar get_local_id_1 Nonvolatile int32,
          Op $ GetLocalId get_local_id_1 1,
          DeclareScalar get_group_id_0 Nonvolatile int32,
          Op $ GetGroupId get_group_id_0 0,
          DeclareScalar get_group_id_1 Nonvolatile int32,
          Op $ GetGroupId get_group_id_1 1,
          DeclareScalar get_group_id_2 Nonvolatile int32,
          Op $ GetGroupId get_group_id_2 2
        ]

    mkTranspose body =
      mconcat
        [ get_ids,
          dec our_array_offset $ vi32 get_group_id_2 * width * height,
          dec odata_offset $
            (basic_odata_offset `quot` primByteSize t) + vi32 our_array_offset,
          dec idata_offset $
            (basic_idata_offset `quot` primByteSize t) + vi32 our_array_offset,
          body
        ]

    lowDimBody x_in_index y_in_index x_out_index y_out_index =
      mconcat
        [ dec x_index x_in_index,
          dec y_index y_in_index,
          dec index_in $ vi32 y_index * width + vi32 x_index,
          when (vi32 x_index .<. width .&&. vi32 y_index .<. height) $
            Write
              block
              (elements $ sExt64 $ vi32 get_local_id_1 * (block_dim + 1) + vi32 get_local_id_0)
              t
              (Space "local")
              Nonvolatile
              $ index
                idata
                (elements $ sExt64 $ vi32 idata_offset + vi32 index_in)
                t
                (Space "global")
                Nonvolatile,
          Op $ Barrier FenceLocal,
          SetScalar x_index $ untyped x_out_index,
          SetScalar y_index $ untyped y_out_index,
          dec index_out $ vi32 y_index * height + vi32 x_index,
          when (vi32 x_index .<. height .&&. vi32 y_index .<. width) $
            Write
              odata
              (elements $ sExt64 (vi32 odata_offset + vi32 index_out))
              t
              (Space "global")
              Nonvolatile
              $ index
                block
                (elements $ sExt64 $ vi32 get_local_id_0 * (block_dim + 1) + vi32 get_local_id_1)
                t
                (Space "local")
                Nonvolatile
        ]

-- | Generate a transpose kernel.  There is special support to handle
-- input arrays with low width, low height, or both.
--
-- Normally when transposing a @[2][n]@ array we would use a @FUT_BLOCK_DIM x
-- FUT_BLOCK_DIM@ group to process a @[2][FUT_BLOCK_DIM]@ slice of the input
-- array. This would mean that many of the threads in a group would be inactive.
-- We try to remedy this by using a special kernel that will process a larger
-- part of the input, by using more complex indexing. In our example, we could
-- use all threads in a group if we are processing @(2/FUT_BLOCK_DIM)@ as large
-- a slice of each rows per group. The variable @mulx@ contains this factor for
-- the kernel to handle input arrays with low height.
--
-- See issue #308 on GitHub for more details.
--
-- These kernels are optimized to ensure all global reads and writes
-- are coalesced, and to avoid bank conflicts in shared memory.  Each
-- thread group transposes a 2D tile of block_dim*2 by block_dim*2
-- elements. The size of a thread group is block_dim/2 by
-- block_dim*2, meaning that each thread will process 4 elements in a
-- 2D tile.  The shared memory array containing the 2D tile consists
-- of block_dim*2 by block_dim*2+1 elements. Padding each row with
-- an additional element prevents bank conflicts from occuring when
-- the tile is accessed column-wise.
mapTransposeKernel ::
  String ->
  Integer ->
  TransposeArgs ->
  PrimType ->
  TransposeType ->
  Kernel
mapTransposeKernel desc block_dim_int args t kind =
  Kernel
    { kernelBody =
        DeclareMem block (Space "local")
          <> Op (LocalAlloc block block_size)
          <> mapTranspose block_dim args t kind,
      kernelUses = uses,
      kernelNumGroups = map untyped num_groups,
      kernelGroupSize = map untyped group_size,
      kernelName = nameFromString name,
      kernelFailureTolerant = True
    }
  where
    pad2DBytes k = k * (k + 1) * primByteSize t
    block_size =
      bytes $
        case kind of
          TransposeSmall -> 1 :: TExp Int64
          -- Not used, but AMD's OpenCL
          -- does not like zero-size
          -- local memory.
          TransposeNormal -> fromInteger $ pad2DBytes $ 2 * block_dim_int
          TransposeLowWidth -> fromInteger $ pad2DBytes block_dim_int
          TransposeLowHeight -> fromInteger $ pad2DBytes block_dim_int
    block_dim = fromInteger block_dim_int :: TExp Int32

    ( odata,
      basic_odata_offset,
      idata,
      basic_idata_offset,
      width,
      height,
      mulx,
      muly,
      num_arrays,
      block
      ) = args

    (num_groups, group_size) =
      case kind of
        TransposeSmall ->
          ( [(num_arrays * width * height) `divUp` (block_dim * block_dim)],
            [block_dim * block_dim]
          )
        TransposeLowWidth ->
          lowDimKernelAndGroupSize block_dim num_arrays width $ height `divUp` muly
        TransposeLowHeight ->
          lowDimKernelAndGroupSize block_dim num_arrays (width `divUp` mulx) height
        TransposeNormal ->
          let actual_dim = block_dim * 2
           in ( [ width `divUp` actual_dim,
                  height `divUp` actual_dim,
                  num_arrays
                ],
                [actual_dim, actual_dim `quot` elemsPerThread, 1]
              )

    uses =
      map
        (`ScalarUse` int32)
        ( namesToList $
            mconcat $
              map
                freeIn
                [ basic_odata_offset,
                  basic_idata_offset,
                  num_arrays,
                  width,
                  height,
                  mulx,
                  muly
                ]
        )
        ++ map MemoryUse [odata, idata]

    name =
      case kind of
        TransposeSmall -> desc ++ "_small"
        TransposeLowHeight -> desc ++ "_low_height"
        TransposeLowWidth -> desc ++ "_low_width"
        TransposeNormal -> desc

lowDimKernelAndGroupSize ::
  TExp Int32 ->
  TExp Int32 ->
  TExp Int32 ->
  TExp Int32 ->
  ([TExp Int32], [TExp Int32])
lowDimKernelAndGroupSize block_dim num_arrays x_elems y_elems =
  ( [ x_elems `divUp` block_dim,
      y_elems `divUp` block_dim,
      num_arrays
    ],
    [block_dim, block_dim, 1]
  )