packages feed

hhlo-0.11.0.0: src/HHLO/Runtime/Execute.hs

module HHLO.Runtime.Execute
    ( execute
    , executeOn
    , executeAsync
    , executeReplicas
    ) where

import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import Foreign.Ptr
import Foreign.Storable
import qualified Foreign.Concurrent as Conc (newForeignPtr)

import Control.Concurrent.Async (mapConcurrently)
import Control.Exception (throwIO)
import Control.Monad (when)
import HHLO.Runtime.PJRT.FFI
import HHLO.Runtime.PJRT.Types
import HHLO.Runtime.PJRT.Registry (isApiAlive)
import HHLO.Runtime.PJRT.Error (PJRTException(..), withErrorMessage)

-- | Execute a compiled program synchronously (blocking).
-- Returns the list of output buffers.
execute :: PJRTApi -> PJRTExecutable -> [PJRTBuffer] -> IO [PJRTBuffer]
execute api exec buffers = withExecPtr exec $ \execPtr -> do
    -- Query the executable's actual output count instead of hardcoding.
    numOutputs <- alloca $ \numOutPtr -> do
        err <- c_pjrtExecutableNumOutputs (unApi api) execPtr numOutPtr
        if err == nullPtr
            then peek numOutPtr
            else do
                withErrorMessage (unApi api) err >>= throwIO . PJRTException
    withBufferPtrs buffers $ \bufPtrs ->
        withArrayLen bufPtrs $ \n bufArr -> do
            allocaArray (fromIntegral numOutputs) $ \outArr -> do
                pokeArray outArr (replicate (fromIntegral numOutputs) nullPtr)
                alloca $ \numOutPtr -> do
                    err <- c_pjrtExecute (unApi api) execPtr
                            (fromIntegral n) bufArr
                            numOutputs outArr numOutPtr
                    if err == nullPtr
                        then do
                            actualNumOut <- peek numOutPtr
                            outPtrs <- peekArray (fromIntegral actualNumOut) outArr
                            mapM (wrapBuffer api) outPtrs
                        else do
                            withErrorMessage (unApi api) err >>= throwIO . PJRTException

-- | Execute on a specific device.
executeOn :: PJRTApi -> PJRTExecutable -> PJRTDevice -> [PJRTBuffer] -> IO [PJRTBuffer]
executeOn api exec dev buffers = withExecPtr exec $ \execPtr -> do
    numOutputs <- alloca $ \numOutPtr -> do
        err <- c_pjrtExecutableNumOutputs (unApi api) execPtr numOutPtr
        if err == nullPtr
            then peek numOutPtr
            else do
                withErrorMessage (unApi api) err >>= throwIO . PJRTException
    withBufferPtrs buffers $ \bufPtrs ->
        withArrayLen bufPtrs $ \n bufArr -> do
            allocaArray (fromIntegral numOutputs) $ \outArr -> do
                pokeArray outArr (replicate (fromIntegral numOutputs) nullPtr)
                alloca $ \numOutPtr -> do
                    err <- c_pjrtExecuteOnDevice (unApi api) execPtr
                            (fromIntegral n) bufArr (unDevice dev)
                            numOutputs outArr numOutPtr
                    if err == nullPtr
                        then do
                            actualNumOut <- peek numOutPtr
                            outPtrs <- peekArray (fromIntegral actualNumOut) outArr
                            mapM (wrapBuffer api) outPtrs
                        else do
                            withErrorMessage (unApi api) err >>= throwIO . PJRTException

-- | Execute asynchronously. Returns immediately with output buffers
-- that may not yet contain valid data. The caller must synchronize
-- via the buffer's ready event or by copying to host.
executeAsync :: PJRTApi -> PJRTExecutable -> [PJRTBuffer] -> IO [PJRTBuffer]
executeAsync = execute  -- For now, same implementation; can be optimized later

-- | Execute the same compiled program concurrently on multiple devices.
-- Each device receives its own input buffers; outputs are gathered
-- in the same order as the input device list.
--
-- This is the recommended API for multi-GPU inference scaling.
-- It launches independent executions via Haskell's async threads,
-- which the underlying PJRT CUDA plugin schedules onto each GPU.
executeReplicas :: PJRTApi -> PJRTExecutable -> [(PJRTDevice, [PJRTBuffer])] -> IO [[PJRTBuffer]]
executeReplicas api exec deviceArgs =
    mapConcurrently (uncurry (executeOn api exec)) deviceArgs

unApi :: PJRTApi -> Ptr PJRTApi
unApi (PJRTApi p) = p

unDevice :: PJRTDevice -> Ptr PJRTDevice
unDevice (PJRTDevice p) = p

-- | Wrap a raw PJRT buffer pointer in a 'ForeignPtr' with a finalizer.
-- The finalizer checks whether the API session is still alive before
-- calling 'PJRT_Buffer_Destroy', preventing segfaults when buffers
-- outlive their client.
wrapBuffer :: PJRTApi -> Ptr PJRTBuffer -> IO PJRTBuffer
wrapBuffer api rawPtr = do
    let apiPtr = unApi api
    fp <- Conc.newForeignPtr rawPtr $ do
        alive <- isApiAlive apiPtr
        when alive $ do
            _ <- c_pjrtBufferDestroy apiPtr rawPtr
            return ()
    return $ PJRTBuffer fp