| {-# LANGUAGE OverloadedStrings #-} |
|
|
| |
| |
| |
| |
|
|
| module LiquidLean.QuantumPiper.IBMGranite |
| ( GraniteModel(..) |
| , GraniteInference(..) |
| , loadGraniteModel |
| , inferenceRequest |
| , streamTokens |
| ) where |
|
|
| import Data.Text (Text) |
| import qualified Data.Text as T |
| import Data.ByteString (ByteString) |
| import qualified Data.ByteString as BS |
| import Data.Map.Strict (Map) |
| import qualified Data.Map.Strict as Map |
| import Data.Word (Word32, Word64) |
| import Data.Vector (Vector) |
| import qualified Data.Vector as V |
| import Data.Aeson (ToJSON, FromJSON, Value, encode, decode, object, (.=)) |
| import GHC.Generics (Generic) |
| import Control.Concurrent.STM (TVar, newTVarIO, readTVarIO, modifyTVar') |
| import Control.Concurrent.Async (async, wait) |
| import Network.HTTP.Client (newManager, defaultManagerSettings, httpLbs, parseRequest, requestBody, requestHeaders, method, RequestBody(..)) |
| import Network.HTTP.Client.TLS (tlsManagerSettings) |
| import System.Environment (lookupEnv) |
| import Data.Time.Clock.POSIX (getPOSIXTime) |
|
|
| |
| |
| |
|
|
| data GraniteModel = GraniteModel |
| { gmModelId :: Text |
| , gmVersion :: Text |
| , gmContext :: Word32 |
| , gmVocabSize :: Word32 |
| , gmWeights :: FilePath |
| , gmQuantization :: Text |
| , gmParameters :: Map Text Value |
| } deriving (Show, Generic) |
| |
| instance ToJSON GraniteModel |
| instance FromJSON GraniteModel |
| |
| data GraniteInference = GraniteInference |
| { giModelId :: Text |
| , giPrompt :: Text |
| , giMaxTokens :: Word32 |
| , giTemperature :: Double |
| , giTopP :: Double |
| , giTopK :: Word32 |
| , giStopSequences :: [Text] |
| , giSystemPrompt :: Maybe Text |
| } deriving (Show, Generic) |
| |
| instance ToJSON GraniteInference |
| instance FromJSON GraniteInference |
| |
| data GraniteResponse = GraniteResponse |
| { grTokens :: [Text] |
| , grCompletionTokens :: Word32 |
| , grPromptTokens :: Word32 |
| , grFinishReason :: Text |
| , grLatency :: Double |
| } deriving (Show, Generic) |
| |
| instance ToJSON GraniteResponse |
| instance FromJSON GraniteResponse |
| |
| |
| |
| |
| |
| loadGraniteModel :: FilePath -> IO (Either String GraniteModel) |
| loadGraniteModel ggufPath = do |
| |
| fileExists <- doesFileExist ggufPath |
| if not fileExists |
| then pure (Left $ "Model file not found: " ++ ggufPath) |
| else do |
| result <- try $ do |
| |
| content <- BS.readFile ggufPath |
| let header = BS.take 4 content |
| |
| if header == "GGUF" |
| then do |
| |
| let modelId = "ibm/granite-13b" |
| let version = "2026-q2" |
| let context = 8192 |
| let vocabSize = 49152 |
| |
| pure (GraniteModel |
| { gmModelId = modelId |
| , gmVersion = version |
| , gmContext = context |
| , gmVocabSize = vocabSize |
| , gmWeights = ggufPath |
| , gmQuantization = "Q4_K" |
| , gmParameters = Map.fromList [] |
| }) |
| else |
| fail "Invalid GGUF header" |
| |
| case result of |
| Left (e :: SomeException) -> pure (Left $ "Failed to load model: " ++ show e) |
| Right model -> pure (Right model) |
| |
| doesFileExist :: FilePath -> IO Bool |
| doesFileExist _ = pure True |
| |
| try :: IO a -> IO (Either SomeException a) |
| try action = (Right <$> action) `catch` (\e -> pure (Left e)) |
| |
| import Control.Exception (catch, SomeException) |
| |
| |
| |
| |
| |
| inferenceRequest :: GraniteModel -> GraniteInference -> IO (Either String GraniteResponse) |
| inferenceRequest model inference = do |
| result <- try $ do |
| |
| manager <- newManager tlsManagerSettings |
| |
| |
| apiKey <- lookupEnv "IBM_GRANITE_API_KEY" |
| |
| case apiKey of |
| Nothing -> fail "IBM_GRANITE_API_KEY not set" |
| Just key -> do |
| |
| let url = "https://api.granite.ibm.com/v1/inference" |
| |
| req <- parseRequest url |
| |
| let request = req |
| { method = "POST" |
| , requestHeaders = |
| [ ("Authorization", "Bearer " <> BS.pack key) |
| , ("Content-Type", "application/json") |
| ] |
| , requestBody = RequestBodyLBS (encode inference) |
| } |
| |
| |
| response <- httpLbs request manager |
| |
| |
| case decode (responseBody response) of |
| Nothing -> fail "Failed to parse Granite response" |
| Just resp -> pure resp |
| |
| case result of |
| Left (e :: SomeException) -> pure (Left $ "Inference failed: " ++ show e) |
| Right response -> pure (Right response) |
| |
| |
| responseBody :: a -> a |
| responseBody x = x |
| |
| |
| |
| |
| |
| streamTokens :: GraniteModel -> GraniteInference -> (Text -> IO ()) -> IO (Either String ()) |
| streamTokens model inference onToken = do |
| result <- try $ do |
| |
| let streamUrl = "https://api.granite.ibm.com/v1/inference/stream" |
| |
| apiKey <- lookupEnv "IBM_GRANITE_API_KEY" |
| |
| case apiKey of |
| Nothing -> fail "IBM_GRANITE_API_KEY not set" |
| Just key -> do |
| |
| manager <- newManager tlsManagerSettings |
| |
| req <- parseRequest streamUrl |
| |
| let request = req |
| { method = "POST" |
| , requestHeaders = |
| [ ("Authorization", "Bearer " <> BS.pack key) |
| , ("Content-Type", "application/json") |
| , ("Accept", "text/event-stream") |
| ] |
| , requestBody = RequestBodyLBS (encode inference) |
| } |
| |
| |
| response <- httpLbs request manager |
| |
| |
| let tokens = parseSSEStream (responseBody response) |
| |
| |
| mapM_ onToken tokens |
| |
| pure () |
| |
| case result of |
| Left (e :: SomeException) -> pure (Left $ "Streaming failed: " ++ show e) |
| Right () -> pure (Right ()) |
| |
| parseSSEStream :: a -> [Text] |
| parseSSEStream _ = [] |
| |
| |
| |
| |
| |
| batchInference :: GraniteModel -> [GraniteInference] -> IO [Either String GraniteResponse] |
| batchInference model inferences = do |
| tasks <- mapM (\inf -> async (inferenceRequest model inf)) inferences |
| mapM wait tasks |
| |
| |
| |
| |
| |
| data GraniteCluster = GraniteCluster |
| { gcModels :: [GraniteModel] |
| , gcLoadBalance :: TVar Int |
| } |
| |
| initGraniteCluster :: [FilePath] -> IO (Either String GraniteCluster) |
| initGraniteCluster ggufPaths = do |
| results <- mapM loadGraniteModel ggufPaths |
| let models = [m | Right m <- results] |
| |
| if null models |
| then pure (Left "Failed to load any Granite models") |
| else do |
| loadBalancer <- newTVarIO 0 |
| pure (Right (GraniteCluster models loadBalancer)) |
| |
| selectGraniteReplica :: GraniteCluster -> IO GraniteModel |
| selectGraniteReplica cluster = do |
| idx <- readTVarIO (gcLoadBalance cluster) |
| let nextIdx = (idx + 1) `mod` length (gcModels cluster) |
| modifyTVar' (gcLoadBalance cluster) (\_ -> nextIdx) |
| pure (gcModels cluster !! idx) |
| |