Skip to content

Commit 80683e6

Browse files
authored
Merge pull request #25 from transformerlab/add/logprobs-encoder-decoder
Implement logprobs for encoder-decoder models
2 parents 0b1a0a5 + 635f7b0 commit 80683e6

2 files changed

Lines changed: 85 additions & 42 deletions

File tree

fastchat/serve/inference.py

Lines changed: 84 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def prepare_logits_processor(
6161
return processor_list
6262

6363

64+
6465
@torch.inference_mode()
6566
def generate_stream(
6667
model,
@@ -107,8 +108,6 @@ def generate_stream(
107108
input_echo_len = len(input_ids)
108109

109110
if model.config.is_encoder_decoder:
110-
if logprobs is not None: # FIXME: Support logprobs for encoder-decoder models.
111-
raise NotImplementedError
112111
encoder_output = model.encoder(
113112
input_ids=torch.as_tensor([input_ids], device=device)
114113
)[0]
@@ -117,6 +116,8 @@ def generate_stream(
117116
dtype=torch.int64,
118117
device=device,
119118
)
119+
# For encoder-decoder models, we track decoder output tokens for logprobs
120+
# (This variable is not used directly but indicates the intent for future enhancements)
120121
else:
121122
start_ids = torch.as_tensor([input_ids], device=device)
122123

@@ -143,34 +144,40 @@ def generate_stream(
143144
past_key_values = out.past_key_values
144145

145146
if logprobs is not None:
146-
# Prefull logprobs for the prompt.
147-
shift_input_ids = start_ids[..., 1:].contiguous()
148-
shift_logits = logits[..., :-1, :].contiguous()
149-
shift_logits = torch.log_softmax(shift_logits, dim=-1).tolist()
150-
for label_id, logit in zip(
151-
shift_input_ids[0].tolist(), shift_logits[0]
152-
):
153-
token_logprobs.append(logit[label_id])
154-
# Add empty top_logprobs during prefill (would need to reconstruct full logits tensor to get these)
155-
top_logprobs_list.append({})
147+
if model.config.is_encoder_decoder:
148+
# For encoder-decoder models, we only compute logprobs for generated tokens
149+
# No prefill logprobs needed since encoder input doesn't contribute to generation logprobs
150+
pass
151+
else:
152+
# Prefull logprobs for the prompt.
153+
shift_input_ids = start_ids[..., 1:].contiguous()
154+
shift_logits = logits[..., :-1, :].contiguous()
155+
shift_logits = torch.log_softmax(shift_logits, dim=-1).tolist()
156+
for label_id, logit in zip(
157+
shift_input_ids[0].tolist(), shift_logits[0]
158+
):
159+
token_logprobs.append(logit[label_id])
160+
# Add empty top_logprobs during prefill (would need to reconstruct full logits tensor to get these)
161+
top_logprobs_list.append({})
156162
else: # decoding
157163
if model.config.is_encoder_decoder:
164+
# For encoder-decoder, use the last generated token or full sequence if interrupted
165+
decoder_input = torch.as_tensor(
166+
[[output_ids[-1]] if not sent_interrupt and len(output_ids) > input_echo_len else output_ids[input_echo_len:]],
167+
device=device,
168+
)
158169
out = model.decoder(
159-
input_ids=torch.as_tensor(
160-
[[token] if not sent_interrupt else output_ids],
161-
device=device,
162-
),
170+
input_ids=decoder_input,
163171
encoder_hidden_states=encoder_output,
164172
use_cache=True,
165173
past_key_values=past_key_values if not sent_interrupt else None,
166174
)
167175
sent_interrupt = False
168-
169176
logits = model.lm_head(out[0])
170177
else:
171178
out = model(
172179
input_ids=torch.as_tensor(
173-
[[token] if not sent_interrupt else output_ids],
180+
[[output_ids[-1]] if not sent_interrupt else output_ids],
174181
device=device,
175182
),
176183
use_cache=True,
@@ -252,30 +259,66 @@ def generate_stream(
252259
)
253260
ret_logprobs = None
254261
if logprobs is not None:
255-
# Calculate the start position for this streaming chunk
256-
if echo:
257-
start_pos = last_sent_token_pos
258-
tokens_to_send = output_ids[start_pos:]
262+
if model.config.is_encoder_decoder:
263+
# For encoder-decoder models, calculate logprobs differently
264+
# We only track generated tokens (not input tokens)
265+
generated_tokens_count = len(output_ids) - input_echo_len
266+
if generated_tokens_count > 0:
267+
# Calculate the start position for this streaming chunk
268+
if echo:
269+
# For echo=True with encoder-decoder, we still only show generated tokens
270+
start_pos = max(last_sent_token_pos, input_echo_len)
271+
else:
272+
start_pos = max(last_sent_token_pos, input_echo_len)
273+
274+
tokens_to_send = output_ids[start_pos:]
275+
276+
# Update last sent position for next stream chunk
277+
last_sent_token_pos = len(output_ids)
278+
279+
# For encoder-decoder, logprobs start from the first generated token
280+
logprobs_start_idx = start_pos - input_echo_len + 1 # +1 because token_logprobs[0] is None
281+
logprobs_end_idx = len(output_ids) - input_echo_len + 1
282+
283+
if logprobs_start_idx < len(token_logprobs) and tokens_to_send:
284+
ret_logprobs = {
285+
"text_offset": [],
286+
"tokens": [tokenizer.decode([token]) for token in tokens_to_send],
287+
"token_logprobs": token_logprobs[logprobs_start_idx:logprobs_end_idx],
288+
"top_logprobs": top_logprobs_list[logprobs_start_idx:logprobs_end_idx],
289+
}
290+
291+
# Compute text_offset for just this chunk
292+
curr_pos = 0
293+
for text in ret_logprobs["tokens"]:
294+
ret_logprobs["text_offset"].append(curr_pos)
295+
curr_pos += len(text)
259296
else:
260-
start_pos = max(last_sent_token_pos, input_echo_len)
261-
tokens_to_send = output_ids[start_pos:]
262-
263-
# Update last sent position for next stream chunk
264-
last_sent_token_pos = len(output_ids)
265-
266-
# Format response with only new tokens
267-
ret_logprobs = {
268-
"text_offset": [],
269-
"tokens": [tokenizer.decode(token) for token in tokens_to_send],
270-
"token_logprobs": token_logprobs[start_pos:],
271-
"top_logprobs": top_logprobs_list[start_pos:],
272-
}
273-
274-
# Compute text_offset for just this chunk
275-
curr_pos = 0
276-
for text in ret_logprobs["tokens"]:
277-
ret_logprobs["text_offset"].append(curr_pos)
278-
curr_pos += len(text)
297+
# Original logic for causal LM models
298+
# Calculate the start position for this streaming chunk
299+
if echo:
300+
start_pos = last_sent_token_pos
301+
tokens_to_send = output_ids[start_pos:]
302+
else:
303+
start_pos = max(last_sent_token_pos, input_echo_len)
304+
tokens_to_send = output_ids[start_pos:]
305+
306+
# Update last sent position for next stream chunk
307+
last_sent_token_pos = len(output_ids)
308+
309+
# Format response with only new tokens
310+
ret_logprobs = {
311+
"text_offset": [],
312+
"tokens": [tokenizer.decode(token) for token in tokens_to_send],
313+
"token_logprobs": token_logprobs[start_pos:],
314+
"top_logprobs": top_logprobs_list[start_pos:],
315+
}
316+
317+
# Compute text_offset for just this chunk
318+
curr_pos = 0
319+
for text in ret_logprobs["tokens"]:
320+
ret_logprobs["text_offset"].append(curr_pos)
321+
curr_pos += len(text)
279322

280323
# TODO: For the issue of incomplete sentences interrupting output, apply a patch and others can also modify it to a more elegant way
281324
if judge_sent_end and stopped and not is_sentence_complete(output):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "transformerlab-inference"
7-
version = "0.2.47"
7+
version = "0.2.48"
88
description = "An open platform for training, serving, and evaluating large language model based chatbots."
99
readme = "README.md"
1010
requires-python = ">=3.8"

0 commit comments

Comments
 (0)