@@ -180,6 +180,43 @@ public void BatchExecutor_BatchedPath_RoutesPerSeqLogitsCorrectly()
180180 }
181181 }
182182
183+ [ Fact ]
184+ public async Task InferenceEngine_ExecutorStepException_CompletesErroredRequestAndContinuesWaiting ( )
185+ {
186+ var model = new ThrowingBatchedStubModel ( "fp-step-error" , badRequestId : "bad" , peakToken : 7 ) ;
187+ var cfg = new SchedulerConfig
188+ {
189+ MaxNumBatchedTokens = 256 ,
190+ MaxNumRunningSequences = 1 ,
191+ MaxPrefillChunkSize = 64 ,
192+ NumBlocks = 4 ,
193+ BlockSize = BlockSize ,
194+ EnablePrefixCaching = false ,
195+ DecodeQuantumTokens = 1 ,
196+ } ;
197+ using var engine = new InferenceEngine ( model , cfg , NullLogger . Instance ) ;
198+
199+ var badSeq = new SequenceState ( "bad" , Enumerable . Range ( 1 , 4 ) . ToList ( ) ,
200+ maxNewTokens : 2 , BlockSize , SamplingConfig . Default ) ;
201+ var goodSeq = new SequenceState ( "good" , Enumerable . Range ( 1 , 4 ) . ToList ( ) ,
202+ maxNewTokens : 2 , BlockSize , SamplingConfig . Default ) ;
203+
204+ var bad = engine . SubmitRequest ( badSeq ) ;
205+ var good = engine . SubmitRequest ( goodSeq ) ;
206+
207+ var ex = await Assert . ThrowsAsync < InvalidOperationException > (
208+ ( ) => bad . Completion . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ) ;
209+ Assert . Contains ( "bad" , ex . Message ) ;
210+
211+ var goodCompletion = await good . Completion . WaitAsync ( TimeSpan . FromSeconds ( 5 ) ) ;
212+ Assert . Equal ( SequenceStatus . FinishedLengthCapped , goodCompletion . Status ) ;
213+ Assert . Equal ( 0 , badSeq . BlockTable . NumBlocks ) ;
214+ Assert . Equal ( SequenceStatus . FinishedError , badSeq . Status ) ;
215+ Assert . Contains ( "bad" , model . ReleasedRequestIds ) ;
216+ Assert . Contains ( "good" , model . ReleasedRequestIds ) ;
217+ Assert . Equal ( cfg . NumBlocks , engine . PoolStats . freeBlocks ) ;
218+ }
219+
183220 // ----- helpers -----
184221
185222 private static SchedulerConfig SmallConfig ( ) => new ( )
@@ -301,6 +338,61 @@ public IReadOnlyList<float[]> ForwardBatch(BatchedForwardContext ctx)
301338 }
302339 }
303340
341+ private sealed class ThrowingBatchedStubModel : IModelArchitecture , IBatchedPagedModel
342+ {
343+ private readonly string _fp ;
344+ private readonly string _badRequestId ;
345+ private readonly int _peak ;
346+
347+ public ThrowingBatchedStubModel ( string fp , string badRequestId , int peakToken )
348+ {
349+ _fp = fp ;
350+ _badRequestId = badRequestId ;
351+ _peak = peakToken ;
352+ Tokenizer = new StubTokenizer ( VocabSize ) ;
353+ }
354+
355+ public List < string > ReleasedRequestIds { get ; } = new ( ) ;
356+
357+ public ModelConfig Config { get ; } = new ModelConfig { VocabSize = VocabSize } ;
358+ public ITokenizer Tokenizer { get ; }
359+ public IMultimodalInjector MultimodalInjector => null ;
360+ public IBackendExecutionPlan ExecutionPlan => null ;
361+ public bool SupportsKVCacheTruncation => true ;
362+ public bool SupportsKVStateSnapshot => true ;
363+ public string KVStateFingerprint => _fp ;
364+ public long ComputeKVBlockByteSize ( int n ) => 2L * NumLayers * NumKVHeads * n * HeadDim * sizeof ( float ) ;
365+ public float [ ] Forward ( int [ ] tokens ) => new float [ VocabSize ] ;
366+ public void ResetKVCache ( ) { }
367+ public void TruncateKVCache ( int n ) { }
368+ public bool TryExtractKVBlock ( int s , int n , Span < byte > dst ) => true ;
369+ public bool TryInjectKVBlock ( int s , int n , ReadOnlySpan < byte > src ) => true ;
370+ public void Dispose ( ) { }
371+
372+ public IReadOnlyList < float [ ] > ForwardBatch ( BatchedForwardContext ctx )
373+ {
374+ for ( int i = 0 ; i < ctx . Sequences . Count ; i ++ )
375+ {
376+ if ( ctx . Sequences [ i ] . RequestId == _badRequestId )
377+ throw new InvalidOperationException ( $ "boom for { _badRequestId } ") ;
378+ }
379+
380+ var result = new float [ ctx . Sequences . Count ] [ ] ;
381+ for ( int i = 0 ; i < result . Length ; i ++ )
382+ {
383+ var logits = new float [ VocabSize ] ;
384+ logits [ _peak ] = 10f ;
385+ result [ i ] = logits ;
386+ }
387+ return result ;
388+ }
389+
390+ public void OnSequenceReleased ( string requestId )
391+ {
392+ ReleasedRequestIds . Add ( requestId ) ;
393+ }
394+ }
395+
304396 private sealed class StubTokenizer : ITokenizer
305397 {
306398 public StubTokenizer ( int vocab )
0 commit comments