@@ -651,13 +651,48 @@ async def _generate_from_intrinsic(
651651 user_params ["tokenizer" ] = self ._tokenizer
652652 generate_input .update (user_params )
653653
654+ want_scores = bool (
655+ model_options .get (ModelOption .LOGITS )
656+ or model_options .get (ModelOption .RAW_LOGITS )
657+ )
658+ if model_options .get (ModelOption .LOGITS ):
659+ generate_input ["output_scores" ] = True
660+ if model_options .get (ModelOption .RAW_LOGITS ):
661+ generate_input ["output_logits" ] = True
662+
663+ # When logits are requested, intercept the raw GenerateDecoderOnlyOutput that
664+ # generate_with_transformers produces internally but never returns (it wraps
665+ # everything into a ChatCompletionResponse). We proxy self._model so that
666+ # .generate() stores the raw output in raw_hf_output_cell before returning it;
667+ # granite_formatters_processing then writes it to mot._meta["hf_output"] for
668+ # _surface_logits in post_processing.
669+ raw_hf_output_cell : list [GenerateDecoderOnlyOutput | None ] = [None ]
670+
671+ model_arg = self ._model
672+ if want_scores :
673+ _real_model = self ._model
674+
675+ class _CapturingModelProxy :
676+ def generate (self_proxy , * args : Any , ** kwargs : Any ) -> Any :
677+ result = cast (Callable [..., Any ], _real_model .generate )(
678+ * args , ** kwargs
679+ )
680+ if isinstance (result , GenerateDecoderOnlyOutput ):
681+ raw_hf_output_cell [0 ] = result
682+ return result
683+
684+ def __getattr__ (self_proxy , name : str ) -> Any :
685+ return getattr (_real_model , name )
686+
687+ model_arg = _CapturingModelProxy () # type: ignore[assignment]
688+
654689 chat_response = asyncio .to_thread (
655690 self ._generate_with_adapter_lock ,
656691 adapter .qualified_name ,
657692 granite_formatters .base .util .generate_with_transformers , # type: ignore
658693 # Passed as args/kwargs to generate.
659694 self ._tokenizer ,
660- self . _model ,
695+ model_arg ,
661696 generate_input ,
662697 other_input ,
663698 )
@@ -681,8 +716,18 @@ async def granite_formatters_processing(
681716 except json .JSONDecodeError as e :
682717 raise Exception (f"Intrinsic did not return a JSON: { chunk } " ) from e
683718
684- # TODO: If we want to support caches, we need to get the GenerateDecoderOnlyOutput. This means we
685- # probably need to break out the pieces from `generate_with_transformers`.
719+ # If logits were requested, stash the intercepted raw output so that
720+ # post_processing/_surface_logits can populate generation.logits/raw_logits.
721+ if want_scores :
722+ if raw_hf_output_cell [0 ] is not None :
723+ mot ._meta ["hf_output" ] = raw_hf_output_cell [0 ]
724+ else :
725+ MelleaLogger .get_logger ().warning (
726+ "ModelOption.LOGITS/RAW_LOGITS requested on intrinsic path but "
727+ "generate_with_transformers did not return a GenerateDecoderOnlyOutput; "
728+ "generation.logits and generation.raw_logits will be None."
729+ )
730+
686731 # processing expects a str or a GenerateDecoderOnlyOutput. Extract the str.
687732 return await self .processing (
688733 mot , res .choices [0 ].message .content , input_ids = input_ids
@@ -941,6 +986,12 @@ async def _generate_from_context_with_kv_cache(
941986 # transformers' generate() requires a tokenizer to decode stop_strings.
942987 generate_kwargs ["tokenizer" ] = self ._tokenizer
943988
989+ kv_scores_kwargs : dict [str , Any ] = {}
990+ if model_options .get (ModelOption .LOGITS ):
991+ kv_scores_kwargs ["output_scores" ] = True
992+ if model_options .get (ModelOption .RAW_LOGITS ):
993+ kv_scores_kwargs ["output_logits" ] = True
994+
944995 chat_response = asyncio .to_thread (
945996 self ._generate_with_adapter_lock ,
946997 "" , # Empty for no adapters.
@@ -951,7 +1002,7 @@ async def _generate_from_context_with_kv_cache(
9511002 past_key_values = merged_cache ,
9521003 attention_mask = attention_mask .to (self ._device ),
9531004 return_dict_in_generate = True ,
954- output_scores = True ,
1005+ ** kv_scores_kwargs ,
9551006 ** generate_kwargs ,
9561007 ** streaming_kwargs , # type: ignore
9571008 ** format_kwargs , # type: ignore
@@ -1112,6 +1163,12 @@ async def _generate_from_context_standard(
11121163 # transformers' generate() requires a tokenizer to decode stop_strings.
11131164 generate_kwargs ["tokenizer" ] = self ._tokenizer
11141165
1166+ scores_kwargs : dict [str , Any ] = {}
1167+ if model_options .get (ModelOption .LOGITS ):
1168+ scores_kwargs ["output_scores" ] = True
1169+ if model_options .get (ModelOption .RAW_LOGITS ):
1170+ scores_kwargs ["output_logits" ] = True
1171+
11151172 chat_response = asyncio .to_thread (
11161173 self ._generate_with_adapter_lock ,
11171174 "" , # Empty for no adapters.
@@ -1121,6 +1178,7 @@ async def _generate_from_context_standard(
11211178 attention_mask = input_ids ["attention_mask" ],
11221179 return_dict_in_generate = True ,
11231180 use_cache = self ._use_caches , # Only create KV cache if caching is enabled
1181+ ** scores_kwargs ,
11241182 ** generate_kwargs ,
11251183 ** streaming_kwargs , # type: ignore
11261184 ** format_kwargs , # type: ignore
@@ -1220,6 +1278,50 @@ async def processing(
12201278 ),
12211279 )
12221280
1281+ def _surface_logits (
1282+ self , mot : ModelOutputThunk , hf_output : GenerateDecoderOnlyOutput
1283+ ) -> None :
1284+ """Populate mot.generation.logits and/or raw_logits from hf_output when requested.
1285+
1286+ Checks ModelOption.LOGITS (processed scores) and ModelOption.RAW_LOGITS (raw
1287+ LM-head logits) in mot._model_options. Skips population when
1288+ num_return_sequences > 1, logging a warning the first time per backend instance.
1289+
1290+ Args:
1291+ mot: The output thunk whose generation metadata will be updated.
1292+ hf_output: The HuggingFace generation output containing scores/logits.
1293+ """
1294+ hf_field = {ModelOption .LOGITS : "scores" , ModelOption .RAW_LOGITS : "logits" }
1295+ for opt , tensors , attr in (
1296+ (ModelOption .LOGITS , hf_output .scores , "logits" ),
1297+ (ModelOption .RAW_LOGITS , hf_output .logits , "raw_logits" ),
1298+ ):
1299+ if not (mot ._model_options and mot ._model_options .get (opt )):
1300+ continue
1301+ if tensors is None :
1302+ MelleaLogger .get_logger ().debug (
1303+ "%s requested but hf_output.%s is None; generation.%s will not be populated." ,
1304+ opt ,
1305+ hf_field [opt ],
1306+ attr ,
1307+ )
1308+ continue
1309+ warn_key = f"nrs_{ opt } "
1310+ if tensors [0 ].shape [0 ] > 1 :
1311+ if warn_key not in self ._warned_about :
1312+ self ._warned_about .add (warn_key )
1313+ MelleaLogger .get_logger ().warning (
1314+ "%s is set but num_return_sequences > 1; "
1315+ "logit tensors are ambiguous across sequences and will not be populated." ,
1316+ opt ,
1317+ )
1318+ else :
1319+ setattr (
1320+ mot .generation ,
1321+ attr ,
1322+ tuple (s .squeeze (0 ).detach ().clone () for s in tensors ),
1323+ )
1324+
12231325 async def post_processing (
12241326 self ,
12251327 mot : ModelOutputThunk ,
@@ -1262,7 +1364,11 @@ class used during generation, if any.
12621364 if (
12631365 self ._use_caches
12641366 and isinstance (hf_output , GenerateDecoderOnlyOutput )
1265- and (hf_output .past_key_values is not None or hf_output .scores is not None )
1367+ and (
1368+ hf_output .past_key_values is not None
1369+ or hf_output .scores is not None
1370+ or hf_output .logits is not None
1371+ )
12661372 ):
12671373 output_complete = hf_output .sequences [0 ]
12681374 kv_cache : DynamicCache | None = hf_output .past_key_values # type: ignore
@@ -1282,29 +1388,19 @@ class used during generation, if any.
12821388 cache_key = id (mot .value )
12831389 self .cache_put (cache_key , cache_info )
12841390
1285- # Surface logits to caller before clearing — scores move to LRU cache;
1286- # when LOGITS=True the tuple views also keep them alive via generation.logits.
1287- if (
1288- hf_output .scores is not None
1289- and mot ._model_options
1290- and mot ._model_options .get (ModelOption .LOGITS )
1291- ):
1292- # squeeze(0): hf_output.scores is (1, vocab_size) per token; normalise to (vocab_size,)
1293- mot .generation .logits = tuple (s .squeeze (0 ) for s in hf_output .scores )
1391+ # Surface logits before clearing — scores move to LRU cache; when
1392+ # LOGITS/RAW_LOGITS=True the tuple views also keep them alive via generation.
1393+ if mot ._model_options :
1394+ self ._surface_logits (mot , hf_output )
12941395
1295- # Clear KV cache and scores from HF output - they're retained via the LRU cache
1296- # (and, when LOGITS=True, also via the views in mot.generation.logits).
1396+ # Clear KV cache and scores from HF output; retained via LRU cache above.
12971397 hf_output .past_key_values = None
12981398 hf_output .scores = None
1299- elif (
1300- isinstance (hf_output , GenerateDecoderOnlyOutput )
1301- and hf_output .scores is not None
1302- and mot ._model_options
1303- and mot ._model_options .get (ModelOption .LOGITS )
1304- ):
1305- # Caching disabled — scores were not moved to LRU, surface them directly
1306- # squeeze(0): normalise (1, vocab_size) per token to (vocab_size,)
1307- mot .generation .logits = tuple (s .squeeze (0 ) for s in hf_output .scores )
1399+ hf_output .logits = None
1400+ elif isinstance (hf_output , GenerateDecoderOnlyOutput ) and mot ._model_options :
1401+ # Caching disabled — surface scores/raw logits directly if requested.
1402+ self ._surface_logits (mot , hf_output )
1403+ hf_output .logits = None
13081404
13091405 # Only scan for tools if we are not doing structured output and tool calls were provided to the model.
13101406 if _format is None and tool_calls :
@@ -1526,6 +1622,12 @@ async def generate_from_raw(
15261622 # transformers' generate() requires a tokenizer to decode stop_strings.
15271623 generate_kwargs ["tokenizer" ] = self ._tokenizer
15281624
1625+ raw_scores_kwargs : dict [str , Any ] = {}
1626+ if model_opts .get (ModelOption .LOGITS ):
1627+ raw_scores_kwargs ["output_scores" ] = True
1628+ if model_opts .get (ModelOption .RAW_LOGITS ):
1629+ raw_scores_kwargs ["output_logits" ] = True
1630+
15291631 _start = time .perf_counter ()
15301632 try :
15311633 outputs = await asyncio .to_thread (
@@ -1536,7 +1638,7 @@ async def generate_from_raw(
15361638 input_ids = inputs ["input_ids" ],
15371639 attention_mask = inputs ["attention_mask" ],
15381640 return_dict_in_generate = True ,
1539- output_scores = True ,
1641+ ** raw_scores_kwargs ,
15401642 ** generate_kwargs ,
15411643 ** format_kwargs ,
15421644 )
@@ -1571,6 +1673,7 @@ async def generate_from_raw(
15711673 agg_prompt = 0
15721674 agg_completion = 0
15731675 want_logits = bool (model_opts and model_opts .get (ModelOption .LOGITS ))
1676+ want_raw_logits = bool (model_opts and model_opts .get (ModelOption .RAW_LOGITS ))
15741677 for i , decoded_result in enumerate (decoded_results ):
15751678 n_prompt_tokens = inputs ["input_ids" ][i ].size (0 ) # type: ignore
15761679 n_completion_tokens = len (sequences_to_decode [i ])
@@ -1593,6 +1696,11 @@ async def generate_from_raw(
15931696 step_scores [i ].detach ().clone () for step_scores in outputs .scores
15941697 )
15951698
1699+ if want_raw_logits and outputs .logits is not None :
1700+ result .generation .raw_logits = tuple (
1701+ step_logits [i ].detach ().clone () for step_logits in outputs .logits
1702+ )
1703+
15961704 action = actions [i ]
15971705 result .parsed_repr = (
15981706 action .parse (result ) if isinstance (action , Component ) else result .value
0 commit comments