Skip to content

Commit bf39b23

Browse files
committed
Add ORTGenAI Engine support
1 parent 599ec67 commit bf39b23

17 files changed

Lines changed: 172 additions & 101 deletions

backends/generative.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package backends
22

3+
import "context"
4+
35
// GuidanceType specifies the constrained-generation strategy.
46
type GuidanceType string
57

@@ -19,8 +21,8 @@ type Guidance struct {
1921
}
2022

2123
type SequenceDelta struct {
22-
Token string
23-
Index int
24+
Token string
25+
Sequence int
2426
}
2527

2628
// Message represents a single message in a conversation.
@@ -30,3 +32,19 @@ type Message struct {
3032
Content string `json:"content"`
3133
ImageURLs []string `json:"image_urls,omitempty"` // File paths or data URIs for multimodal support
3234
}
35+
36+
// GenerativeModel abstracts either a generative session or engine.
37+
type GenerativeModel interface {
38+
Generate(ctx context.Context, inputs [][]Message, tools []string, options *GenerativeOptions) (chan SequenceDelta, chan error, error)
39+
GetStatistics() PipelineStatistics
40+
Destroy() error
41+
}
42+
43+
// GenerativeOptions contains settings for text generation.
44+
type GenerativeOptions struct {
45+
MaxLength int
46+
Temperature *float64
47+
TopP *float64
48+
Seed *int
49+
Guidance *Guidance
50+
}

backends/model.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,20 @@ func LoadModel(path string, onnxFilename string, options *options.Options, isGen
4242
IsGenerative: isGenerative,
4343
}
4444

45-
if !isGenerative {
45+
if isGenerative {
46+
// creation of the session. Only one output (either token or sentence embedding).
47+
if options.Backend != "ORT" {
48+
return nil, fmt.Errorf("generative models are only supported with ORT backend currently")
49+
}
50+
if onnxFilename != "" {
51+
return nil, fmt.Errorf("onnx filename should not be provided for generative models as we currently rely on genai_config for the onnx backend")
52+
}
53+
54+
err := createORTGenerativeSession(model, options)
55+
if err != nil {
56+
return nil, err
57+
}
58+
} else {
4659
err := loadModelConfig(model)
4760
if err != nil {
4861
return nil, err
@@ -55,18 +68,6 @@ func LoadModel(path string, onnxFilename string, options *options.Options, isGen
5568
if tkErr != nil {
5669
return nil, tkErr
5770
}
58-
} else {
59-
// creation of the session. Only one output (either token or sentence embedding).
60-
if options.Backend != "ORT" {
61-
return nil, fmt.Errorf("generative models are only supported with ORT backend currently")
62-
}
63-
if onnxFilename != "" {
64-
return nil, fmt.Errorf("onnx filename should not be provided for generative models as we currently rely on genai_config for the onnx backend")
65-
}
66-
err := createORTGenerativeSession(model, options)
67-
if err != nil {
68-
return nil, err
69-
}
7071
}
7172

7273
model.Destroy = func() error {

backends/model_gomlx.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func createGoMLXModelBackend(model *Model, options *options.Options) error {
4545
modelParsed, err = parser.ParseReader(model.OnnxReader)
4646
} else {
4747
modelParsed, err = parser.ParseFile(fileutil.PathJoinSafe(model.Path, model.OnnxPath))
48-
if err != nil {
48+
if err != nil && modelParsed != nil {
4949
modelParsed = modelParsed.WithBaseDir(model.Path)
5050
}
5151
}
@@ -97,7 +97,7 @@ func createGoMLXModelBackend(model *Model, options *options.Options) error {
9797
return errors.Join(contextErr, modelParsed.Close())
9898
}
9999

100-
maxCache, batchBuckets, sequenceBuckets := getCacheAndBucketSizes(options, model, config)
100+
maxCache, batchBuckets, sequenceBuckets := getCacheAndBucketSizes(options, config)
101101
exec.SetMaxCache(maxCache)
102102

103103
model.GoMLXModel = &GoMLXModel{
@@ -121,7 +121,7 @@ func createGoMLXModelBackend(model *Model, options *options.Options) error {
121121
return err
122122
}
123123

124-
func getCacheAndBucketSizes(options *options.Options, model *Model, backend string) (int, []int, []int) {
124+
func getCacheAndBucketSizes(options *options.Options, backend string) (int, []int, []int) {
125125
bucketsSpecified := false
126126
// Use configured buckets or fall back to defaults.
127127
batchBuckets := defaultBatchBuckets

backends/model_ort.go

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212
"strconv"
1313
"strings"
14+
"sync"
1415

1516
ort "github.com/yalue/onnxruntime_go"
1617
"golang.org/x/sync/errgroup"
@@ -23,11 +24,14 @@ import (
2324
type ORTModel struct {
2425
Session *ort.DynamicAdvancedSession
2526
GenerativeSession *ortgenai.Session
27+
GenerativeEngine *ortgenai.Engine
2628
SessionOptions *ort.SessionOptions
2729
Options *options.OrtOptions
2830
Destroy func() error
2931
}
3032

33+
var generativeBackendMutex = sync.Mutex{}
34+
3135
func mapORTOptions(options *options.Options) ([]string, map[string]map[string]string, error) {
3236
if options == nil || options.ORTOptions == nil {
3337
return []string{}, map[string]map[string]string{}, nil // let default EPs be used
@@ -87,11 +91,54 @@ func mapORTOptions(options *options.Options) ([]string, map[string]map[string]st
8791
}
8892

8993
func createORTGenerativeSession(model *Model, options *options.Options) error {
90-
9194
if strings.HasPrefix(model.Path, "s3:") {
9295
return errors.New("ORT Gen AI does not support S3 paths. Please download the model to a local directory and try again")
9396
}
9497

98+
err := initialiseORTGenAI(options)
99+
if err != nil {
100+
return err
101+
}
102+
103+
providers, providerOptions, err := mapORTOptions(options)
104+
if err != nil {
105+
return fmt.Errorf("error mapping ORT options for generative session: %w", err)
106+
}
107+
108+
if options.ORTOptions.UseEngine {
109+
ortGenAiEngine, err := ortgenai.CreateEngineWithOptions(model.Path, providers, providerOptions)
110+
if err != nil {
111+
return fmt.Errorf("error creating ortgenai engine: %w", err)
112+
}
113+
model.ORTModel = &ORTModel{
114+
GenerativeEngine: ortGenAiEngine,
115+
Options: options.ORTOptions,
116+
Destroy: func() error {
117+
ortGenAiEngine.Destroy()
118+
return nil
119+
},
120+
}
121+
} else {
122+
ortGenAiSession, err := ortgenai.CreateSessionWithOptions(model.Path, providers, providerOptions)
123+
if err != nil {
124+
return fmt.Errorf("error creating ortgenai session: %w", err)
125+
}
126+
model.ORTModel = &ORTModel{
127+
GenerativeSession: ortGenAiSession,
128+
Options: options.ORTOptions,
129+
Destroy: func() error {
130+
ortGenAiSession.Destroy()
131+
return nil
132+
},
133+
}
134+
}
135+
return nil
136+
}
137+
138+
func initialiseORTGenAI(options *options.Options) error {
139+
generativeBackendMutex.Lock()
140+
defer generativeBackendMutex.Unlock()
141+
95142
if !ortgenai.IsInitialized() {
96143
if options == nil || options.ORTOptions == nil {
97144
return fmt.Errorf("ORT options must be provided to initialize ortgenai")
@@ -124,22 +171,6 @@ func createORTGenerativeSession(model *Model, options *options.Options) error {
124171
return fmt.Errorf("error initializing the ort genai environment: %w", err)
125172
}
126173
}
127-
providers, providerOptions, err := mapORTOptions(options)
128-
if err != nil {
129-
return fmt.Errorf("error mapping ORT options for generative session: %w", err)
130-
}
131-
ortGenAiSession, err := ortgenai.CreateSessionWithOptions(model.Path, providers, providerOptions)
132-
if err != nil {
133-
return fmt.Errorf("error creating ortgenai session: %w", err)
134-
}
135-
model.ORTModel = &ORTModel{
136-
GenerativeSession: ortGenAiSession,
137-
Options: options.ORTOptions,
138-
Destroy: func() error {
139-
ortGenAiSession.Destroy()
140-
return nil
141-
},
142-
}
143174
return nil
144175
}
145176

@@ -156,8 +187,9 @@ func runGenerativeORTSessionOnBatch(ctx context.Context, batch *PipelineBatch, p
156187
}
157188

158189
session := p.Model.ORTModel.GenerativeSession
159-
if session == nil {
160-
return nil, nil, errors.New("ORT generative session is not initialized")
190+
engine := p.Model.ORTModel.GenerativeEngine
191+
if session == nil && engine == nil {
192+
return nil, nil, errors.New("ORT generative session/engine is not initialized")
161193
}
162194

163195
inputs, ok := batch.InputValues.([][]ortgenai.Message)
@@ -183,6 +215,10 @@ func runGenerativeORTSessionOnBatch(ctx context.Context, batch *PipelineBatch, p
183215
generateCtx, cancel := context.WithCancel(ctx)
184216

185217
if batch.Images != nil {
218+
if session == nil {
219+
cancel()
220+
return nil, nil, errors.New("multimodal generation requires a session, but only engine is initialized")
221+
}
186222
images, ok := batch.Images.(*ortgenai.Images)
187223
if !ok {
188224
cancel()
@@ -194,7 +230,11 @@ func runGenerativeORTSessionOnBatch(ctx context.Context, batch *PipelineBatch, p
194230
return nil, nil, fmt.Errorf("error during multimodal generation start: %w", err)
195231
}
196232
} else {
197-
ortTokenStream, ortErrorStream, err = session.Generate(generateCtx, inputs, tools, &ortgenai.GenerationOptions{MaxLength: maxLength, Temperature: temperature, TopP: topP, Seed: seed, Guidance: ortGuidance})
233+
if engine != nil {
234+
ortTokenStream, ortErrorStream, err = engine.Generate(generateCtx, inputs, tools, &ortgenai.GenerationOptions{MaxLength: maxLength, Temperature: temperature, TopP: topP, Seed: seed, Guidance: ortGuidance})
235+
} else {
236+
ortTokenStream, ortErrorStream, err = session.Generate(generateCtx, inputs, tools, &ortgenai.GenerationOptions{MaxLength: maxLength, Temperature: temperature, TopP: topP, Seed: seed, Guidance: ortGuidance})
237+
}
198238
if err != nil {
199239
cancel()
200240
return nil, nil, fmt.Errorf("error during generation start: %w", err)
@@ -237,14 +277,14 @@ func runGenerativeORTSessionOnBatch(ctx context.Context, batch *PipelineBatch, p
237277
if !ok {
238278
return
239279
}
240-
idx := tokenDelta.Sequence
241-
if completeSequences[idx] {
280+
sequence := tokenDelta.Sequence
281+
if completeSequences[sequence] {
242282
// Already complete; ignore further tokens for this sequence.
243283
continue
244284
}
245285
if tokenDelta.EOSReached {
246286
// EOS terminates sequence; no token content to forward.
247-
completeSequences[idx] = true
287+
completeSequences[sequence] = true
248288
completedCount++
249289
if completedCount == totalSequences {
250290
cancel()
@@ -254,26 +294,26 @@ func runGenerativeORTSessionOnBatch(ctx context.Context, batch *PipelineBatch, p
254294
}
255295

256296
// Forward token immediately.
257-
tokenStream <- SequenceDelta{Token: tokenDelta.Token, Index: idx}
297+
tokenStream <- SequenceDelta{Token: tokenDelta.Token, Sequence: sequence}
258298

259299
// Detect stop sequences using a bounded rolling window.
260300
// Window size is maxStopLen + len(current token) to catch:
261301
// - stops spanning the boundary between previous tail and this token
262302
// - stops fully contained inside a single (possibly long) token
263303
if len(filteredStops) > 0 && maxStopLen > 0 {
264-
tail := tails[idx] + tokenDelta.Token
304+
tail := tails[sequence] + tokenDelta.Token
265305
keep := maxStopLen + len(tokenDelta.Token)
266306
if keep < maxStopLen {
267307
keep = maxStopLen
268308
}
269309
if len(tail) > keep {
270310
tail = tail[len(tail)-keep:]
271311
}
272-
tails[idx] = tail
312+
tails[sequence] = tail
273313

274314
for _, s := range filteredStops {
275315
if strings.Contains(tail, s) {
276-
completeSequences[idx] = true
316+
completeSequences[sequence] = true
277317
completedCount++
278318
if completedCount == totalSequences {
279319
cancel()

backends/model_ort_disabled.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111

1212
type ORTModel struct {
1313
Destroy func() error
14-
GenerativeSession disabledGenerativeSession // placeholder when ORT disabled
14+
GenerativeSession *disabledGenerativeSession // placeholder when ORT disabled
15+
GenerativeEngine *disabledGenerativeEngine // placeholder when ORT disabled
1516
}
1617

1718
func createORTModelBackend(_ *Model, _ *options.Options) error {
@@ -46,12 +47,19 @@ func CreateMessagesORT(_ *PipelineBatch, _ any, _ string) error {
4647
return errors.New("ORT is not enabled")
4748
}
4849

49-
type disabledGenerativeSession struct{}
50+
type (
51+
disabledGenerativeSession struct{}
52+
disabledGenerativeEngine struct{}
53+
)
5054

5155
func (*disabledGenerativeSession) GetStatistics() disabledStatistics {
5256
return disabledStatistics{}
5357
}
5458

59+
func (*disabledGenerativeEngine) GetStatistics() disabledStatistics {
60+
return disabledStatistics{}
61+
}
62+
5563
type disabledStatistics struct {
5664
AvgPrefillSeconds float64
5765
TokensPerSecond float64

docker-bake.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ variable "GO_VERSION" { default = "1.26.2" }
55
variable "GOTESTSUM_VERSION" { default = "1.13.0" }
66
variable "GOPJRT_VERSION" { default = "0.98.0" }
77
variable "ONNXRUNTIME_VERSION" { default = "1.24.4" }
8-
variable "ONNXRUNTIME_GENAI_VERSION" { default = "0.12.2" }
8+
variable "ONNXRUNTIME_GENAI_VERSION" { default = "0.13.1" }
99
variable "JAX_CUDA_VERSION" { default = "0.9.1" }
1010

1111
target "base" {

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
module github.com/knights-analytics/hugot
22

3-
go 1.25.0
3+
go 1.26.0
44

55
require (
66
github.com/daulet/tokenizers v1.27.0
77
github.com/gomlx/go-huggingface v0.3.5
88
github.com/gomlx/gomlx v0.27.3
99
github.com/gomlx/onnx-gomlx v0.4.2
10-
github.com/knights-analytics/ortgenai v0.2.0
10+
github.com/knights-analytics/ortgenai v0.3.0
1111
github.com/stretchr/testify v1.11.1
1212
github.com/viant/afs v1.30.0
1313
github.com/yalue/onnxruntime_go v1.27.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ github.com/janpfeifer/go-benchmarks v0.1.1 h1:gLLy07/JrOKSnMWeUxSnjTdhkglgmrNR2I
5858
github.com/janpfeifer/go-benchmarks v0.1.1/go.mod h1:5AagXCOUzevvmYFQalcgoa4oWPyH1IkZNckolGWfiSM=
5959
github.com/janpfeifer/must v0.2.0 h1:yWy1CE5gtk1i2ICBvqAcMMXrCMqil9CJPkc7x81fRdQ=
6060
github.com/janpfeifer/must v0.2.0/go.mod h1:S6c5Yg/YSMR43cJw4zhIq7HFMci90a7kPY9XA4c8UIs=
61-
github.com/knights-analytics/ortgenai v0.2.0 h1:WOZAHxbvlHswydkDrBJi/XnqdzGT1kErUA4hnzoCajA=
62-
github.com/knights-analytics/ortgenai v0.2.0/go.mod h1:NsxP23iC77IP6q+gRTC+v79v3hyv7fKH1iSa7D+DoVk=
61+
github.com/knights-analytics/ortgenai v0.3.0 h1:ruHiWzxGnKVowlDQ4zKA3dafLLmFxus6R1vqA0rp6Ss=
62+
github.com/knights-analytics/ortgenai v0.3.0/go.mod h1:lSbQsRP5wY5NS+4W5CUGhdxjTzERQkR7WprAFxrBSt4=
6363
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
6464
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
6565
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

hugot.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,9 @@ func NewPipeline[T backends.Pipeline](s *Session, pipelineConfig backends.Pipeli
192192
defer pipelineLock.Unlock()
193193

194194
_, getError := GetPipeline[T](s, pipelineConfig.Name)
195-
var notFoundError *pipelineNotFoundError
196195
if getError == nil {
197196
return pipeline, fmt.Errorf("pipeline %s has already been initialised", pipelineConfig.Name)
198-
} else if !errors.As(getError, &notFoundError) {
197+
} else if _, ok := errors.AsType[*pipelineNotFoundError](getError); !ok {
199198
return pipeline, getError
200199
}
201200

hugot_ort_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,11 @@ func TestTextGenerationPipelineORTCuda(t *testing.T) {
331331
if os.Getenv("CI") != "" {
332332
t.SkipNow()
333333
}
334-
session, err := NewORTSession(t.Context(), options.WithCuda(map[string]string{
335-
"device_id": "0",
336-
}))
334+
session, err := NewORTSession(t.Context(),
335+
options.WithCuda(map[string]string{
336+
"device_id": "0",
337+
}),
338+
options.WithUseEngine())
337339
checkT(t, err)
338340
defer func(session *Session) {
339341
destroyErr := session.Destroy()

0 commit comments

Comments
 (0)