Current section
Files
Jump to
Current section
Files
lib/bumblebee/text/text_generation.ex
defmodule Bumblebee.Text.TextGeneration do
@moduledoc false
alias Bumblebee.Shared
alias Bumblebee.Text
def generation(model_info, tokenizer, %Text.GenerationConfig{} = generation_config, opts \\ []) do
opts =
Keyword.validate!(opts, [
:compile,
defn_options: [],
preallocate_params: false,
stream: false,
stream_done: false
])
%{model: model, params: params, spec: spec} = model_info
Shared.validate_architecture!(spec, [
:for_conditional_generation,
:for_causal_language_modeling
])
preallocate_params = opts[:preallocate_params]
defn_options = opts[:defn_options]
compile =
if compile = opts[:compile] do
compile
|> Keyword.validate!([:batch_size, :sequence_length])
|> Shared.require_options!([:batch_size, :sequence_length])
end
batch_size = compile[:batch_size]
sequence_length = compile[:sequence_length]
tokenizer =
Bumblebee.configure(tokenizer,
length: sequence_length,
pad_direction: :left,
return_token_type_ids: false,
return_length: true
)
generate_fun =
Bumblebee.Text.Generation.build_generate(model, spec, generation_config,
ignore_output: opts[:stream]
)
batch_keys = Shared.sequence_batch_keys(sequence_length)
Nx.Serving.new(
fn batch_key, defn_options ->
params = Shared.maybe_preallocate(params, preallocate_params, defn_options)
scope = {:generate, batch_key}
generate_fun =
Shared.compile_or_jit(generate_fun, scope, defn_options, compile != nil, fn ->
{:sequence_length, sequence_length} = batch_key
inputs = %{
"input_ids" => Nx.template({batch_size, sequence_length}, :u32),
"attention_mask" => Nx.template({batch_size, sequence_length}, :u32),
"seed" => Nx.template({batch_size}, :s64)
}
[params, inputs]
end)
fn inputs ->
inputs = Shared.maybe_pad(inputs, batch_size)
generate_fun.(params, inputs) |> Shared.serving_post_computation()
end
end,
defn_options
)
|> Nx.Serving.batch_size(batch_size)
|> Nx.Serving.process_options(batch_keys: batch_keys)
|> Nx.Serving.client_preprocessing(fn input ->
if opts[:stream] do
Shared.validate_input_for_stream!(input)
end
{inputs, multi?} = Shared.validate_serving_input!(input, &validate_input/1)
texts = Enum.map(inputs, & &1.text)
seed = Enum.map(inputs, & &1.seed) |> Nx.tensor(type: :s64, backend: Nx.BinaryBackend)
inputs =
Nx.with_default_backend(Nx.BinaryBackend, fn ->
Bumblebee.apply_tokenizer(tokenizer, texts)
end)
{input_length, inputs} = Map.pop!(inputs, "length")
input_padded_length = Nx.axis_size(inputs["input_ids"], 1)
inputs = Map.put(inputs, "seed", seed)
batch_key = Shared.sequence_batch_key_for_inputs(inputs, sequence_length)
batch = [inputs] |> Nx.Batch.concatenate() |> Nx.Batch.key(batch_key)
{batch, {multi?, input_length, input_padded_length}}
end)
|> add_postprocessing(opts[:stream], opts[:stream_done], tokenizer)
end
defp validate_input(text) when is_binary(text), do: validate_input(%{text: text})
defp validate_input(%{text: text} = input) do
{:ok, %{text: text, seed: input[:seed] || :erlang.system_time()}}
end
defp validate_input(%{} = input) do
{:error, "expected the input map to have :text key, got: #{inspect(input)}"}
end
defp validate_input(input) do
{:error, "expected either a string or a map, got: #{inspect(input)}"}
end
@doc false
def add_postprocessing(serving, stream, stream_done, tokenizer)
def add_postprocessing(serving, false, _stream_done, tokenizer) do
Nx.Serving.client_postprocessing(
serving,
fn {%{token_ids: token_ids, length: length}, _metadata},
{multi?, input_length, input_padded_length} ->
decoded = Bumblebee.Tokenizer.decode(tokenizer, token_ids)
output_length = Nx.to_flat_list(length)
input_length = Nx.to_flat_list(input_length)
Enum.zip_with(
[decoded, output_length, input_length],
fn [decoded, output_length, input_length] ->
token_summary = token_summary(input_length, input_padded_length, output_length)
%{results: [%{text: decoded, token_summary: token_summary}]}
end
)
|> Shared.normalize_output(multi?)
end
)
end
def add_postprocessing(serving, true, stream_done, tokenizer) do
serving
|> Nx.Serving.streaming(hooks: [:token])
|> Nx.Serving.client_postprocessing(fn stream,
{false = _multi?, input_length, input_padded_length} ->
[input_length] = Nx.to_flat_list(input_length)
Stream.transform(stream, %{tokens: [], consumed_size: 0, finished?: false}, fn
_event, %{finished?: true} = state ->
{:halt, state}
{:token, %{token_id: token_id, finished?: finished?, length: output_length}}, state ->
token_id = Nx.to_number(token_id[0])
finished? = Nx.to_number(finished?[0]) == 1
state = %{state | tokens: state.tokens ++ [token_id], finished?: finished?}
chunk = pending_chunk(tokenizer, state)
{items, state} =
cond do
# When the sequence is finished early or we reach a newline,
# we flush the cache
finished? or String.ends_with?(chunk, "\n") ->
{[chunk], %{state | tokens: [], consumed_size: 0}}
# CJK characters are tokenized atomically, so we can emit
# the chunk
chunk != "" and cjk_codepoint?(last_codepoint(chunk)) ->
state = update_in(state.consumed_size, &(&1 + byte_size(chunk)))
{[chunk], state}
# Emit chunk until the space. We need to keep tokens,
# because certain tokenizers do not encode whitespace in
# tokens and they add a space based on previous tokens
space_idx = find_last_occurrence(chunk, " ") ->
if space_idx > 0 do
chunk = binary_slice(chunk, 0, space_idx)
state = update_in(state.consumed_size, &(&1 + space_idx))
{[chunk], state}
else
{[], state}
end
true ->
{[], state}
end
if finished? and stream_done do
output_length = Nx.to_number(output_length[0])
token_summary = token_summary(input_length, input_padded_length, output_length)
done = {:done, %{token_summary: token_summary}}
{items ++ [done], state}
else
{items, state}
end
end)
end)
end
defp token_summary(input_length, input_padded_length, output_length) do
%{
input: input_length,
output: output_length,
padding: input_padded_length - input_length
}
end
defp pending_chunk(tokenizer, state) do
text = Bumblebee.Tokenizer.decode(tokenizer, state.tokens)
binary_slice(text, state.consumed_size..-1//1)
end
defp find_last_occurrence(string, pattern) do
case :binary.matches(string, pattern) do
[] -> nil
matches -> matches |> List.last() |> elem(0)
end
end
defp last_codepoint(<<codepoint::utf8>>), do: codepoint
defp last_codepoint(<<_::utf8, rest::binary>>), do: last_codepoint(rest)
defp cjk_codepoint?(codepoint) do
# The specific ranges originated in [1] and are generally mirrored
# in other tokenizers using WordPiece. Also see [2].
#
# [1]: https://github.com/google-research/bert/blob/eedf5716ce1268e56f0a50264a88cafad334ac61/tokenization.py#L264-L284
# [2]: https://github.com/google-research/bert/blob/eedf5716ce1268e56f0a50264a88cafad334ac61/multilingual.md#tokenization
codepoint in 0x4E00..0x9FFF or
codepoint in 0x3400..0x4DBF or
codepoint in 0x20000..0x2A6DF or
codepoint in 0x2A700..0x2B73F or
codepoint in 0x2B740..0x2B81F or
codepoint in 0x2B820..0x2CEAF or
codepoint in 0xF900..0xFAFF or
codepoint in 0x2F800..0x2FA1F
end
end