keel-abi-0.1.0.0: src/Keel/Abi/DLPack.hs
-- | Managed ownership over DLPack's versioned exchange tensor.
--
-- The DLPack contract: the producer hands over a
-- 'DLManagedTensorVersioned' (usually inside a @dltensor_versioned@
-- PyCapsule); the consumer checks the major version, uses the data, and
-- calls the deleter exactly once. This module packages both sides the
-- same way "Keel.Abi.Arrow" does for Arrow structs: consumption under
-- 'Control.Exception.finally', production through a process-wide deleter
-- trampoline plus a 'Foreign.StablePtr.StablePtr'-carried cleanup.
module Keel.Abi.DLPack
( AbiError (..)
-- * Consumer side
, consumeManagedTensor
-- * Producer side
, newManagedTensor
) where
import Control.Exception (Exception, SomeException, finally, mask_, onException, throwIO, try)
import Data.Int (Int64)
import Data.Word (Word32, Word64)
import Foreign.Marshal.Alloc (free, mallocBytes)
import Foreign.Ptr (FunPtr, Ptr, nullPtr, plusPtr)
import Foreign.StablePtr
( StablePtr
, castPtrToStablePtr
, castStablePtrToPtr
, deRefStablePtr
, freeStablePtr
, newStablePtr
)
import Foreign.Storable (peek, poke, pokeElemOff, sizeOf)
import System.IO.Unsafe (unsafePerformIO)
import Keel.Abi.DLPack.Raw
-- | Failure modes of the exchange protocol itself.
newtype AbiError = DLPackMajorUnsupported Word32
-- ^ The producer filled the struct under a DLPack major version
-- newer than these bindings ('dlpackMajorVersion') understand.
deriving (Eq, Show)
instance Exception AbiError
-- | Take ownership of a produced tensor: run the action on the peeked
-- struct, then invoke the deleter — also when the action throws. If the
-- producer's major version is newer than 'dlpackMajorVersion', the
-- tensor is deleted unused (the version\/deleter prologue is stable
-- across majors by design) and 'DLPackMajorUnsupported' is thrown.
consumeManagedTensor
:: Ptr DLManagedTensorVersioned
-> (DLManagedTensorVersioned -> IO a)
-> IO a
consumeManagedTensor p act = do
m <- peek p
let major = dlverMajor (mtvVersion m)
if major > dlpackMajorVersion
then do
callTensorDeleter p
throwIO (DLPackMajorUnsupported major)
else act m `finally` callTensorDeleter p
foreign import ccall "wrapper"
wrapDeleter
:: (Ptr DLManagedTensorVersioned -> IO ())
-> IO (FunPtr (Ptr DLManagedTensorVersioned -> IO ()))
-- One process-wide deleter, never freed: runs the cleanup carried in
-- manager_ctx, then frees the struct block itself (the deleter deletes
-- @self@ per spec). The cleanup runs under 'try' with the exception
-- dropped: the deleter is invoked by foreign code, and a Haskell
-- exception escaping into a C caller is undefined behaviour.
{-# NOINLINE deleterTrampoline #-}
deleterTrampoline :: FunPtr (Ptr DLManagedTensorVersioned -> IO ())
deleterTrampoline = unsafePerformIO . wrapDeleter $ \p -> do
m <- peek p
let sp = castPtrToStablePtr (mtvManagerCtx m) :: StablePtr (IO ())
cleanup <- deRefStablePtr sp
_ <- try @SomeException cleanup
freeStablePtr sp
free p
-- | Allocate and fill a 'DLManagedTensorVersioned' for handoff to a
-- consumer. The shape array lives in the same allocation as the struct;
-- the tensor is CPU-device, compact row-major (null strides), zero byte
-- offset, version 'dlpackMajorVersion'.'dlpackMinorVersion'. The cleanup
-- runs exactly once — from the consumer's deleter call, on whatever
-- thread that happens — and must free\/unpin the data buffer; the
-- struct block frees itself afterwards. It must not throw: a thrown
-- exception is caught and discarded (the C caller cannot receive it).
newManagedTensor
:: DLDataType
-> [Int64] -- ^ shape (row-major, compact)
-> Ptr () -- ^ data
-> Word64 -- ^ flags ('dlpackFlagReadOnly' \/ 'dlpackFlagIsCopied' \/ 0)
-> IO () -- ^ cleanup, owns the data buffer
-> IO (Ptr DLManagedTensorVersioned)
newManagedTensor dt shape dat flags cleanup = mask_ $ do
let ndim = length shape
structSz = sizeOf (undefined :: DLManagedTensorVersioned)
p <- mallocBytes (structSz + ndim * 8)
flip onException (free p) $ do
let shapeP = p `plusPtr` structSz
mapM_ (uncurry (pokeElemOff shapeP)) (zip [0 ..] shape)
sp <- newStablePtr cleanup
poke p
DLManagedTensorVersioned
{ mtvVersion = DLPackVersion dlpackMajorVersion dlpackMinorVersion
, mtvManagerCtx = castStablePtrToPtr sp
, mtvDeleter = deleterTrampoline
, mtvFlags = flags
, mtvTensor =
DLTensor
{ dltData = dat
, dltDevice = DLDevice kDLCPU 0
, dltNDim = fromIntegral ndim
, dltDType = dt
, dltShape = shapeP
, dltStrides = nullPtr
, dltByteOffset = 0
}
}
pure p