@@ -24,6 +24,7 @@ import (
2424 "strconv"
2525 "strings"
2626 "sync"
27+ "sync/atomic"
2728 "time"
2829
2930 "github.com/blevesearch/bleve/v2"
@@ -52,6 +53,13 @@ const (
5253 defaultNumSamplesPerCentroid = minSamplesPerCentroid
5354
5455 trainerWaitOnKV = 10 * time .Second
56+
57+ // sampleChannelBuffer decouples the KV scanner goroutines from the
58+ // BuildDocumentEx workers so neither side stalls on the other.
59+ sampleChannelBuffer = 256
60+ // sampleBuildWorkers is the number of goroutines that run BuildDocumentEx
61+ // in parallel inside trainOnSamples.
62+ sampleBuildWorkers = 4
5563)
5664
5765type vectorIndexTrainer struct {
@@ -65,6 +73,8 @@ type vectorIndexTrainer struct {
6573 worker * samplingWorker
6674 bleveDest * BleveDest
6775
76+ trainStats TrainingStats
77+
6878 doneCh chan struct {}
6979 closeCh chan struct {}
7080}
@@ -113,6 +123,10 @@ func initTrainer(bleveDest *BleveDest, kvconfig map[string]interface{}) trainer
113123 return nil
114124}
115125
126+ func (t * vectorIndexTrainer ) WriteJSON (w io.Writer ) {
127+ t .trainStats .WriteJSON (w )
128+ }
129+
116130func (t * vectorIndexTrainer ) wait () <- chan struct {} {
117131 return t .doneCh
118132}
@@ -156,7 +170,7 @@ func (r *samplingWorkerRegistry) getOrCreateWorker(indexName string) (*samplingW
156170 if ! ok {
157171 worker = & samplingWorker {
158172 ref : 1 ,
159- sampleCh : make (chan * sample , 1 ),
173+ sampleCh : make (chan * sample , sampleChannelBuffer ),
160174 doneCh : make (chan struct {}),
161175 closeCh : make (chan struct {}),
162176 copyCh : make (chan struct {}),
@@ -191,9 +205,10 @@ type sample struct {
191205
192206// samplingWorkerRunConfig holds the inputs for configuring a samplingWorker obj.
193207type samplingWorkerRunConfig struct {
194- params * samplingParams
195- vecIndex bleve.TrainableIndex
196- cluster kvCluster
208+ params * samplingParams
209+ vecIndex bleve.TrainableIndex
210+ cluster kvCluster
211+ trainStats * TrainingStats
197212}
198213
199214type samplingParams struct {
@@ -287,6 +302,7 @@ func (w *samplingWorker) configure(c *samplingWorkerRunConfig) {
287302 w .vecIndex = c .vecIndex
288303 w .params = c .params
289304 w .cluster = c .cluster
305+ w .trainStats = c .trainStats
290306}
291307
292308// run performs the sampling run: opens a sampling scan per collection, reads
@@ -321,19 +337,29 @@ func (w *samplingWorker) run() {
321337 default :
322338 }
323339
340+ fetchStart := time .Now ()
324341 doc , err := iterators [i ].Next ()
325342 if err != nil {
326343 errs [i ] = err
327344 return
328345 }
346+ atomic .AddUint64 (& w .trainStats .TotKVFetchTimeInNs , uint64 (time .Since (fetchStart )))
347+
329348 if doc == nil {
330349 // iterator exhausted before reaching sampleLimit
331350 continue
332351 }
333- w .sampleCh <- & sample {
352+
353+ select {
354+ case w .sampleCh <- & sample {
334355 cid : i ,
335356 id : doc .ID ,
336357 value : doc .Val ,
358+ }:
359+ case <- w .closeCh :
360+ log .Warnf ("trainer_vector: stopped sampling for collection %s " +
361+ "due to close signal" , w .params .collectionNames [i ])
362+ return
337363 }
338364 }
339365 }(i )
@@ -501,12 +527,13 @@ func (t *vectorIndexTrainer) computeSampleLimitsForSources(agent *gocbcore.Agent
501527
502528// trainOnSamples consumes samples from ch, builds Bleve documents with
503529// collection scope/UID in extras[sample.cid], indexes them into a training
504- // batch, and calls vecIndex.Train(batch) when doneCh is closed. returns error
530+ // batch, and calls vecIndex.Train(batch) once all samples are consumed.
531+ // sampleBuildWorkers goroutines run BuildDocumentEx in parallel; a single
532+ // serializer goroutine calls batch.Index to avoid races on the batch.
505533func (t * vectorIndexTrainer ) trainOnSamples (ch chan * sample , defaultType string ,
506- extras [][]byte , vecIndex bleve.TrainableIndex , doneCh chan struct {} ) error {
534+ extras [][]byte , vecIndex bleve.TrainableIndex ) error {
507535 batch := vecIndex .NewBatch ()
508536
509- // set the centroid count for the training process
510537 trainingParams := & index.TrainingParams {
511538 NumCentroids : t .numCentroids ,
512539 }
@@ -515,43 +542,88 @@ func (t *vectorIndexTrainer) trainOnSamples(ch chan *sample, defaultType string,
515542 return err
516543 }
517544 batch .SetInternal ([]byte (index .TrainingKey ), bytes )
545+ trainStart := time .Now ()
518546 err = vecIndex .Train (batch )
519547 if err != nil {
520548 return err
521549 }
550+ atomic .AddUint64 (& t .trainStats .TotBatchTrainTimeInNs , uint64 (time .Since (trainStart )))
522551
523- for {
524- select {
525- case <- doneCh :
526- select {
527- case <- t .closeCh :
528- log .Warnf ("trainer_vector: stopped training for %s due to close signal" ,
529- t .partitionName )
530- return fmt .Errorf ("training stopped due to close signal" )
531- default :
532- }
552+ type builtDoc struct {
553+ key string
554+ doc * BleveDocument
555+ }
556+ docCh := make (chan builtDoc , sampleBuildWorkers * 4 )
533557
534- err := vecIndex .Train (batch )
535- if err != nil {
536- return err
537- }
538- return nil
539- case sample := <- ch :
540- if sample != nil {
541- doc , key , err := t .bleveDest .bleveDocConfig .BuildDocumentEx (
542- []byte (sample .id ), sample .value , defaultType ,
543- cbgt .DEST_EXTRAS_TYPE_GOCBCORE_SCOPE_COLLECTION , nil , extras [sample .cid ])
544- if err != nil {
545- return err
546- }
558+ var (
559+ wg sync.WaitGroup
560+ buildErr error
561+ errOnce sync.Once
562+ )
547563
548- err = batch .Index (string (key ), doc )
549- if err != nil {
550- return err
564+ for i := 0 ; i < sampleBuildWorkers ; i ++ {
565+ wg .Add (1 )
566+ go func () {
567+ defer wg .Done ()
568+ for {
569+ select {
570+ case <- t .closeCh :
571+ return
572+ case s , ok := <- ch :
573+ if ! ok || s == nil {
574+ return
575+ }
576+ buildStart := time .Now ()
577+ doc , key , err := t .bleveDest .bleveDocConfig .BuildDocumentEx (
578+ []byte (s .id ), s .value , defaultType ,
579+ cbgt .DEST_EXTRAS_TYPE_GOCBCORE_SCOPE_COLLECTION , nil , extras [s .cid ])
580+ if err != nil {
581+ errOnce .Do (func () { buildErr = err })
582+ return
583+ }
584+ atomic .AddUint64 (& t .trainStats .TotDocBuildTimeInNs , uint64 (time .Since (buildStart )))
585+
586+ select {
587+ case docCh <- builtDoc {key : string (key ), doc : doc }:
588+ case <- t .closeCh :
589+ return
590+ }
551591 }
552592 }
593+ }()
594+ }
595+
596+ // wait for all the documents to be indexed into the batch
597+ go func () {
598+ wg .Wait ()
599+ close (docCh )
600+ }()
601+
602+ for bd := range docCh {
603+ indexStart := time .Now ()
604+ err := batch .Index (bd .key , bd .doc )
605+ if err != nil {
606+ return err
553607 }
608+ atomic .AddUint64 (& t .trainStats .TotBatchTrainTimeInNs , uint64 (time .Since (indexStart )))
609+ }
610+
611+ if buildErr != nil {
612+ return buildErr
554613 }
614+
615+ select {
616+ case <- t .closeCh :
617+ log .Warnf ("trainer_vector: stopped training for %s due to close signal" ,
618+ t .partitionName )
619+ return fmt .Errorf ("training stopped due to close signal" )
620+ default :
621+ }
622+
623+ trainStart = time .Now ()
624+ err = vecIndex .Train (batch )
625+ atomic .AddUint64 (& t .trainStats .TotBatchTrainTimeInNs , uint64 (time .Since (trainStart )))
626+ return err
555627}
556628
557629// trainedIndexConfig carries everything needed to create and train a trained index.
@@ -573,7 +645,9 @@ type trainedIndexConfig struct {
573645// is closed so other BleveDests can copy the trained index.
574646func (t * vectorIndexTrainer ) createTrainedIndex (cfg * trainedIndexConfig ) error {
575647 var err error
648+ samplingStart := time .Now ()
576649 defer func () {
650+ atomic .AddUint64 (& t .trainStats .TotTrainedIndexBuildTimeInNs , uint64 (time .Since (samplingStart )))
577651 cfg .worker .success = (err == nil )
578652 close (cfg .worker .copyCh )
579653 }()
@@ -654,15 +728,16 @@ func (t *vectorIndexTrainer) createTrainedIndex(cfg *trainedIndexConfig) error {
654728 scopeName : cfg .scopeName ,
655729 sourceName : t .bleveDest .sourceName ,
656730 },
657- cluster : & gocbClusterAdapter {cluster : cluster },
731+ cluster : & gocbClusterAdapter {cluster : cluster },
732+ trainStats : & t .trainStats ,
658733 })
659734 }
660735
661736 go cfg .worker .run ()
662737
663738 // single training thread
664739 return t .trainOnSamples (cfg .worker .sampleCh , cfg .defaultType , extraOpts ,
665- cfg .vecIndex , cfg . worker . doneCh )
740+ cfg .vecIndex )
666741}
667742
668743func (t * vectorIndexTrainer ) markTrainingComplete (index bleve.TrainableIndex ) error {
@@ -821,6 +896,7 @@ func (t *vectorIndexTrainer) acquireSamples() {
821896 return
822897 }
823898
899+ start := time .Now ()
824900 if ic , ok := worker .vecIndex .(bleve.IndexFileCopyable ); ok && worker .success {
825901 dest , ok := t .bleveDest .bindex .(bleve.IndexFileCopyable )
826902 if ! ok {
@@ -835,8 +911,8 @@ func (t *vectorIndexTrainer) acquireSamples() {
835911 }
836912 log .Printf ("trainer_vector: finished copying trained index from" +
837913 " source partition %s" , worker .vecIndex .Name ())
914+ atomic .AddUint64 (& t .trainStats .TotTrainedIndexCopyTimeInNs , uint64 (time .Since (start )))
838915 }
839-
840916}
841917
842918// trainedIndexWriter implements the writer used when copying the trained index
0 commit comments