Skip to content

Commit 3dc58cb

Browse files
authored
Ensure num_logits_to_keep is always set to 1 for generate calls (#1681)
* support scalar tensor tolist() * gemma support num_logits_to_keep * set num_logits_to_keep in phi3_v and chatterbox too * set num_logits_to_keep in qwen2_vl and multi_modality * allow num_logits_to_keep to be used in encoder-decoder models * Deduplicate set logic
1 parent 857b29a commit 3dc58cb

9 files changed

Lines changed: 58 additions & 17 deletions

File tree

packages/transformers/src/models/chatterbox/modeling_chatterbox.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class ChatterboxModel extends ChatterboxPreTrainedModel {
4949
// Generic generation parameters
5050
generation_config = null,
5151
logits_processor = null,
52+
num_logits_to_keep = null,
5253

5354
// Speaker embeddings/features (useful for re-using pre-computed speaker data)
5455
audio_features = null, // float32[batch_size,sequence_length,1024]
@@ -118,6 +119,7 @@ export class ChatterboxModel extends ChatterboxPreTrainedModel {
118119
attention_mask,
119120
generation_config,
120121
logits_processor,
122+
num_logits_to_keep,
121123
},
122124
false,
123125
);

packages/transformers/src/models/florence2/modeling_florence2.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class Florence2ForConditionalGeneration extends Florence2PreTrainedModel
7979

8080
inputs_embeds,
8181
decoder_inputs_embeds,
82+
num_logits_to_keep = null,
8283
}) {
8384
if (!inputs_embeds) {
8485
({ inputs_embeds, attention_mask } = await this._prepare_inputs_embeds({
@@ -108,6 +109,7 @@ export class Florence2ForConditionalGeneration extends Florence2PreTrainedModel
108109
encoder_attention_mask: attention_mask,
109110
encoder_hidden_states: encoder_outputs,
110111
past_key_values,
112+
num_logits_to_keep,
111113
};
112114
return await decoder_forward(this, decoderFeeds, true);
113115
}

packages/transformers/src/models/gemma3n/modeling_gemma3n.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class Gemma3nForConditionalGeneration extends Gemma3nPreTrainedModel {
3838
// Generic generation parameters
3939
generation_config = null,
4040
logits_processor = null,
41+
num_logits_to_keep = null,
4142

4243
// TODO: needed?
4344
...kwargs
@@ -84,6 +85,7 @@ export class Gemma3nForConditionalGeneration extends Gemma3nPreTrainedModel {
8485
position_ids,
8586
generation_config,
8687
logits_processor,
88+
num_logits_to_keep,
8789
},
8890
true,
8991
);

packages/transformers/src/models/modeling_utils.js

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,31 @@ export function addPastKeyValues(self, decoderFeeds, pastKeyValues) {
12941294
return new DynamicCache(entries);
12951295
}
12961296

1297+
/**
1298+
* Sets `num_logits_to_keep` on `model_inputs` if the decoder session declares it as an input
1299+
* and it has not already been set.
1300+
*
1301+
* `num_logits_to_keep` specifies how many trailing prompt logits the model computes:
1302+
* - `0n` (or unset) computes logits for the entire sequence — used for prefill/scoring.
1303+
* - `1n` computes only the last token's logits — used during autoregressive generation,
1304+
* since only the last prompt token's logits are needed to sample the next token. For long
1305+
* sequences, computing all logits uses a lot of memory, so `1n` significantly reduces the
1306+
* memory footprint.
1307+
* - Any other positive integer keeps the last `num_logits_to_keep` logits.
1308+
*
1309+
* @param {PreTrainedModel} self The model instance.
1310+
* @param {Record<string, any>} model_inputs The model inputs to mutate.
1311+
* @param {bigint} value The value to set (typically `1n` for generation, `0n` as a fallback).
1312+
* @private
1313+
*/
1314+
export function setNumLogitsToKeep(self, model_inputs, value) {
1315+
if (model_inputs.num_logits_to_keep) return;
1316+
const session = self.sessions['decoder_model_merged'] ?? self.sessions['model'];
1317+
if (session?.inputNames.includes('num_logits_to_keep')) {
1318+
model_inputs.num_logits_to_keep = new Tensor('int64', [value], []);
1319+
}
1320+
}
1321+
12971322
/**
12981323
* Forward pass of a decoder model.
12991324
* @param {Object} self The decoder model.
@@ -1321,14 +1346,8 @@ export async function decoder_forward(self, model_inputs, is_encoder_decoder = f
13211346
new_model_inputs.position_ids = create_position_ids(new_model_inputs, past_key_values, start_index);
13221347
}
13231348

1324-
if (session.inputNames.includes('num_logits_to_keep') && !new_model_inputs.num_logits_to_keep) {
1325-
// `num_logits_to_keep` specifies the number of prompt logits to calculate during generation.
1326-
// If unset (or 0), all logits will be calculated. If an integer value, only last `num_logits_to_keep`
1327-
// logits will be calculated. During generation, the default is 1 because only the logits of the last
1328-
// prompt token are needed for generation. For long sequences, the logits for the entire sequence may
1329-
// use a lot of memory so, setting `num_logits_to_keep=1` will reduce memory footprint significantly.
1330-
new_model_inputs.num_logits_to_keep = new Tensor('int64', [0n], []);
1331-
}
1349+
// Fallback for non-generation forward calls (e.g. prefill scoring): compute all logits.
1350+
setNumLogitsToKeep(self, new_model_inputs, 0n);
13321351

13331352
// Unpack the `past_key_values` object into model inputs
13341353
addPastKeyValues(self, new_model_inputs, past_key_values);
@@ -1353,6 +1372,7 @@ export async function decoder_forward(self, model_inputs, is_encoder_decoder = f
13531372
* @param {DynamicCache} [params.past_key_values=null]
13541373
* @param {Object} [params.generation_config=null]
13551374
* @param {Object} [params.logits_processor=null]
1375+
* @param {Tensor} [params.num_logits_to_keep=null]
13561376
* @returns {Promise<Tensor>} The model's output tensor
13571377
* @private
13581378
*/
@@ -1377,6 +1397,7 @@ export async function generic_text_to_text_forward(
13771397
// Generic generation parameters
13781398
generation_config = null,
13791399
logits_processor = null,
1400+
num_logits_to_keep = null,
13801401

13811402
// Additional parameters
13821403
...kwargs
@@ -1454,6 +1475,7 @@ export async function generic_text_to_text_forward(
14541475
position_ids,
14551476
generation_config,
14561477
logits_processor,
1478+
num_logits_to_keep,
14571479
},
14581480
true,
14591481
);
@@ -1552,12 +1574,7 @@ export function create_position_ids(model_inputs, past_key_values = null, start_
15521574
export function decoder_prepare_inputs_for_generation(self, input_ids, model_inputs, generation_config) {
15531575
const past_length = model_inputs.past_key_values ? model_inputs.past_key_values.get_seq_length() : 0;
15541576

1555-
// During generation, only the last token's logits are needed. Setting num_logits_to_keep=1
1556-
// avoids computing logits for the entire sequence, significantly reducing memory usage.
1557-
const session = self.sessions['decoder_model_merged'] ?? self.sessions['model'];
1558-
if (session?.inputNames.includes('num_logits_to_keep') && !model_inputs.num_logits_to_keep) {
1559-
model_inputs.num_logits_to_keep = new Tensor('int64', [1n], []);
1560-
}
1577+
setNumLogitsToKeep(self, model_inputs, 1n);
15611578

15621579
if (!model_inputs.attention_mask) {
15631580
// If the attention mask is not provided, we attempt to infer based on provided inputs
@@ -1606,6 +1623,8 @@ export function encoder_decoder_prepare_inputs_for_generation(self, input_ids, m
16061623
input_ids = input_ids.map((x) => [x.at(-1)]);
16071624
}
16081625

1626+
setNumLogitsToKeep(self, model_inputs, 1n);
1627+
16091628
return {
16101629
...model_inputs,
16111630
decoder_input_ids: toI64Tensor(input_ids),

packages/transformers/src/models/multi_modality/modeling_multi_modality.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PreTrainedModel, decoder_forward } from '../modeling_utils.js';
1+
import { PreTrainedModel, decoder_forward, setNumLogitsToKeep } from '../modeling_utils.js';
22
import { sessionRun } from '../session.js';
33
import { pick } from '../../utils/core.js';
44
import { RawImage } from '../../utils/image.js';
@@ -73,6 +73,8 @@ export class MultiModalityCausalLM extends MultiModalityPreTrainedModel {
7373
prepare_inputs_for_generation(input_ids, model_inputs, generation_config) {
7474
const has_past_key_values = !!model_inputs.past_key_values;
7575

76+
setNumLogitsToKeep(this, model_inputs, 1n);
77+
7678
if (generation_config.guidance_scale !== null && generation_config.guidance_scale > 1) {
7779
if (has_past_key_values) {
7880
model_inputs.input_ids = cat([model_inputs.input_ids, model_inputs.input_ids], 0);

packages/transformers/src/models/phi3_v/modeling_phi3_v.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class Phi3VForCausalLM extends Phi3VPreTrainedModel {
2929
// Generic generation parameters
3030
generation_config = null,
3131
logits_processor = null,
32+
num_logits_to_keep = null,
3233

3334
// TODO: needed?
3435
...kwargs
@@ -65,6 +66,7 @@ export class Phi3VForCausalLM extends Phi3VPreTrainedModel {
6566
position_ids,
6667
generation_config,
6768
logits_processor,
69+
num_logits_to_keep,
6870
},
6971
false,
7072
);

packages/transformers/src/models/qwen2_vl/modeling_qwen2_vl.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { PreTrainedModel, cumsum_masked_fill, default_merge_input_ids_with_image_features } from '../modeling_utils.js';
1+
import {
2+
PreTrainedModel,
3+
cumsum_masked_fill,
4+
default_merge_input_ids_with_image_features,
5+
setNumLogitsToKeep,
6+
} from '../modeling_utils.js';
27
import { sessionRun } from '../session.js';
38
import { stack, Tensor, ones_like, zeros } from '../../utils/tensor.js';
49
import { max } from '../../utils/maths.js';
@@ -285,6 +290,8 @@ export class Qwen2VLForConditionalGeneration extends Qwen2VLPreTrainedModel {
285290
}
286291

287292
prepare_inputs_for_generation(input_ids, model_inputs, generation_config) {
293+
setNumLogitsToKeep(this, model_inputs, 1n);
294+
288295
// Overwritten -- in specific circumstances we don't want to forward image inputs to the model
289296
if (!model_inputs.attention_mask || model_inputs.position_ids) {
290297
return model_inputs;

packages/transformers/src/utils/tensor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ export class Tensor {
973973
*/
974974
function reshape(data, dimensions) {
975975
const totalElements = data.length;
976-
const dimensionSize = dimensions.reduce((a, b) => a * b);
976+
const dimensionSize = dimensions.length === 0 ? 1 : dimensions.reduce((a, b) => a * b);
977977

978978
if (totalElements !== dimensionSize) {
979979
throw Error(`cannot reshape array of size ${totalElements} into shape (${dimensions})`);

packages/transformers/tests/utils/tensor.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ describe("Tensor operations", () => {
369369
});
370370

371371
describe("tolist", () => {
372+
it("should work with scalar tensors", () => {
373+
const t1 = new Tensor("float32", [42], []);
374+
const arr = t1.tolist();
375+
expect(arr).toBe(42);
376+
});
372377
it("should return nested arrays for a 2D tensor", () => {
373378
const t1 = new Tensor("float32", [1, 2, 3, 4], [2, 2]);
374379
const arr = t1.tolist();

0 commit comments

Comments
 (0)