Add tiny random LFM3-Omni model for optimum-intel tests
#1
by rkazants - opened
- .gitattributes +1 -0
- audio_tokenizer/config.json +28 -0
- audio_tokenizer/model.safetensors +3 -0
- chat_template.jinja +296 -0
- config.json +267 -0
- configuration_lfm3.py +132 -0
- configuration_lfm3_omni.py +300 -0
- configuration_lfm3_vl.py +211 -0
- configuration_liquid_codec.py +131 -0
- feature_extraction_lfm3_omni.py +202 -0
- generation_config.json +10 -0
- image_processing_lfm3_vl.py +293 -0
- lfm3_conv.py +202 -0
- model.safetensors +3 -0
- modeling_lfm3.py +882 -0
- modeling_lfm3_omni.py +1863 -0
- modeling_lfm3_vl.py +965 -0
- modeling_liquid_codec.py +381 -0
- processing_lfm3_omni.py +769 -0
- processor_config.json +101 -0
- tokenizer.json +3 -0
- tokenizer_config.json +25 -0
- video_processing_lfm3_vl.py +396 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
audio_tokenizer/config.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"LiquidCodecModel"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoModel": "modeling_liquid_codec.LiquidCodecModel"
|
| 7 |
+
},
|
| 8 |
+
"decoder_window": 30,
|
| 9 |
+
"dtype": "float32",
|
| 10 |
+
"encoder_conv_dim": 16,
|
| 11 |
+
"encoder_dim": 16,
|
| 12 |
+
"fsq_levels": [
|
| 13 |
+
8,
|
| 14 |
+
7,
|
| 15 |
+
6,
|
| 16 |
+
6
|
| 17 |
+
],
|
| 18 |
+
"initializer_range": 0.02,
|
| 19 |
+
"model_type": "liquid_codec",
|
| 20 |
+
"n_q": 12,
|
| 21 |
+
"quantizer_dim": 16,
|
| 22 |
+
"sampling_rate": 24000,
|
| 23 |
+
"stft_frame_stack": 6,
|
| 24 |
+
"stft_hop_length": 16,
|
| 25 |
+
"stft_n_fft": 64,
|
| 26 |
+
"stft_win_length": 64,
|
| 27 |
+
"transformers_version": "5.20.0"
|
| 28 |
+
}
|
audio_tokenizer/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9d329bbdcf38f5c88a57ea6cddcc8f4cb96f2285387b6a13855f7ac54711f3b2
|
| 3 |
+
size 7676072
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{{- bos_token -}}
|
| 2 |
+
{%- set preserve_thinking = preserve_thinking | default(false) -%}
|
| 3 |
+
{%- set enable_thinking = enable_thinking | default(false) -%}
|
| 4 |
+
|
| 5 |
+
{%- macro render_value(v) -%}
|
| 6 |
+
{%- if v is string -%}
|
| 7 |
+
{{- v -}}
|
| 8 |
+
{%- elif v is boolean -%}
|
| 9 |
+
{{- "true" if v else "false" -}}
|
| 10 |
+
{%- elif v is none -%}
|
| 11 |
+
{{- "null" -}}
|
| 12 |
+
{%- elif v is mapping or (v is sequence and v is not string) -%}
|
| 13 |
+
{{- v | tojson -}}
|
| 14 |
+
{%- else -%}
|
| 15 |
+
{{- v -}}
|
| 16 |
+
{%- endif -%}
|
| 17 |
+
{%- endmacro -%}
|
| 18 |
+
|
| 19 |
+
{%- macro parse_content(content) -%}
|
| 20 |
+
{%- if content is string -%}
|
| 21 |
+
{{- content -}}
|
| 22 |
+
{%- elif content is mapping and content.get("type") == "image" -%}
|
| 23 |
+
{{- "<|vision_start|><|image|><|vision_end|>" -}}
|
| 24 |
+
{%- elif content is mapping and content.get("type") == "video" -%}
|
| 25 |
+
{{- "<|vision_start|><|video|><|vision_end|>" -}}
|
| 26 |
+
{%- elif content is mapping and content.get("type") in ["audio", "audio_url", "input_audio"] -%}
|
| 27 |
+
{{- "<|vision_start|><|audio|><|vision_end|>" -}}
|
| 28 |
+
{%- elif content is mapping and content.get("type") == "audio_out" -%}
|
| 29 |
+
{{- "<|audio_out|>" -}}
|
| 30 |
+
{%- elif content is mapping and content.get("type") == "text" -%}
|
| 31 |
+
{{- (content.get("text") or "") | string -}}
|
| 32 |
+
{%- elif content is mapping -%}
|
| 33 |
+
{{- content | tojson -}}
|
| 34 |
+
{%- elif content is iterable -%}
|
| 35 |
+
{%- set _ns = namespace(result="") -%}
|
| 36 |
+
{%- for item in content -%}
|
| 37 |
+
{%- if item is string -%}
|
| 38 |
+
{%- set _ns.result = _ns.result + item -%}
|
| 39 |
+
{%- elif item is mapping and item.get("type") == "image" -%}
|
| 40 |
+
{%- set _ns.result = _ns.result + "<|vision_start|><|image|><|vision_end|>" -%}
|
| 41 |
+
{%- elif item is mapping and item.get("type") == "video" -%}
|
| 42 |
+
{%- set _ns.result = _ns.result + "<|vision_start|><|video|><|vision_end|>" -%}
|
| 43 |
+
{%- elif item is mapping and item.get("type") in ["audio", "audio_url", "input_audio"] -%}
|
| 44 |
+
{%- set _ns.result = _ns.result + "<|vision_start|><|audio|><|vision_end|>" -%}
|
| 45 |
+
{%- elif item is mapping and item.get("type") == "audio_out" -%}
|
| 46 |
+
{%- set _ns.result = _ns.result + "<|audio_out|>" -%}
|
| 47 |
+
{%- elif item is mapping and item.get("type") == "text" -%}
|
| 48 |
+
{%- set _ns.result = _ns.result + ((item.get("text") or "") | string) -%}
|
| 49 |
+
{%- else -%}
|
| 50 |
+
{%- set _ns.result = _ns.result + (item | tojson) -%}
|
| 51 |
+
{%- endif -%}
|
| 52 |
+
{%- endfor -%}
|
| 53 |
+
{{- _ns.result -}}
|
| 54 |
+
{%- endif -%}
|
| 55 |
+
{%- endmacro -%}
|
| 56 |
+
|
| 57 |
+
{%- macro render_tool_calls(tool_calls) -%}
|
| 58 |
+
{%- for tool_call in tool_calls -%}
|
| 59 |
+
{%- if not loop.first -%}{{- "\n\n" -}}{%- endif -%}
|
| 60 |
+
{%- set func = tool_call["function"] if "function" in tool_call else tool_call -%}
|
| 61 |
+
{%- set func_args = func.get("arguments") -%}
|
| 62 |
+
{%- if func_args is string and (func_args | trim) not in ["", "{}", "null"] -%}
|
| 63 |
+
{{- raise_exception("Tool call arguments must be a mapping, got a JSON-encoded string: parse arguments with json.loads() before applying the chat template") -}}
|
| 64 |
+
{%- endif -%}
|
| 65 |
+
{{- "<tool_call>\n<function=" + func["name"] + ">\n" -}}
|
| 66 |
+
{%- if func_args is mapping -%}
|
| 67 |
+
{%- for arg_name, arg_value in func_args.items() -%}
|
| 68 |
+
{{- "<parameter=" + arg_name + ">\n" + render_value(arg_value) + "\n</parameter>\n" -}}
|
| 69 |
+
{%- endfor -%}
|
| 70 |
+
{%- endif -%}
|
| 71 |
+
{{- "</function>\n</tool_call>" -}}
|
| 72 |
+
{%- endfor -%}
|
| 73 |
+
{%- endmacro -%}
|
| 74 |
+
|
| 75 |
+
{%- macro tool_to_json(tool) -%}
|
| 76 |
+
{%- set fn = tool.function if tool.function is defined else tool -%}
|
| 77 |
+
{%- set ns_t = namespace(first=true) -%}
|
| 78 |
+
{{- "{" -}}
|
| 79 |
+
{%- for k, v in fn.items() -%}
|
| 80 |
+
{%- if k != "defer_loading" -%}
|
| 81 |
+
{%- if not ns_t.first -%}{{- ", " -}}{%- endif -%}
|
| 82 |
+
{%- set ns_t.first = false -%}
|
| 83 |
+
{{- k | tojson -}}{{- ": " -}}{{- v | tojson -}}
|
| 84 |
+
{%- endif -%}
|
| 85 |
+
{%- endfor -%}
|
| 86 |
+
{{- "}" -}}
|
| 87 |
+
{%- endmacro -%}
|
| 88 |
+
|
| 89 |
+
{#- a tool_reference content part (vLLM, SGLang) names a tool from `tools`,
|
| 90 |
+
usually a deferred one; it renders as that tool's definition -#}
|
| 91 |
+
{%- macro is_tool_reference(item) -%}
|
| 92 |
+
{{- "1" if item is mapping and item.get("type") == "tool_reference" else "" -}}
|
| 93 |
+
{%- endmacro -%}
|
| 94 |
+
|
| 95 |
+
{%- macro referenced_tool_to_json(name) -%}
|
| 96 |
+
{%- set _ns = namespace(found=none) -%}
|
| 97 |
+
{%- for tool in tools or [] -%}
|
| 98 |
+
{%- set fn = tool.function if tool.function is defined else tool -%}
|
| 99 |
+
{%- if fn.name == name -%}
|
| 100 |
+
{%- set _ns.found = fn -%}
|
| 101 |
+
{%- endif -%}
|
| 102 |
+
{%- endfor -%}
|
| 103 |
+
{%- if _ns.found is none -%}
|
| 104 |
+
{{- raise_exception("tool_reference names a tool that is not in tools: " + name | string) -}}
|
| 105 |
+
{%- endif -%}
|
| 106 |
+
{{- tool_to_json(_ns.found) -}}
|
| 107 |
+
{%- endmacro -%}
|
| 108 |
+
|
| 109 |
+
{%- macro render_tool_response(content) -%}
|
| 110 |
+
{#- content that already carries the tag (pre-formatted by the caller) is
|
| 111 |
+
passed through as-is instead of being wrapped a second time -#}
|
| 112 |
+
{%- if content is string and "<tool_response>" in content -%}
|
| 113 |
+
{{- content -}}
|
| 114 |
+
{%- else -%}
|
| 115 |
+
{{- "<tool_response>\n" -}}
|
| 116 |
+
{%- if content is string -%}
|
| 117 |
+
{{- content -}}
|
| 118 |
+
{%- elif content is mapping -%}
|
| 119 |
+
{{- parse_content(content) -}}
|
| 120 |
+
{%- elif content is iterable -%}
|
| 121 |
+
{%- for item in content -%}
|
| 122 |
+
{%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
|
| 123 |
+
{%- if is_tool_reference(item) -%}
|
| 124 |
+
{#- consecutive references share one <additional_tools> block -#}
|
| 125 |
+
{%- if loop.first or not is_tool_reference(loop.previtem) -%}
|
| 126 |
+
{{- "<additional_tools>\n" -}}
|
| 127 |
+
{%- endif -%}
|
| 128 |
+
{{- referenced_tool_to_json(item.name) -}}
|
| 129 |
+
{%- if loop.last or not is_tool_reference(loop.nextitem) -%}
|
| 130 |
+
{{- "\n</additional_tools>" -}}
|
| 131 |
+
{%- endif -%}
|
| 132 |
+
{%- elif item is mapping and item.result is defined and item.result is mapping and item.result.tools is defined -%}
|
| 133 |
+
{{- "<additional_tools>\n" -}}
|
| 134 |
+
{%- set _tl = item.result.tools -%}
|
| 135 |
+
{%- if _tl is string -%}
|
| 136 |
+
{{- _tl -}}
|
| 137 |
+
{%- elif _tl is mapping -%}
|
| 138 |
+
{{- tool_to_json(_tl) -}}
|
| 139 |
+
{%- else -%}
|
| 140 |
+
{%- for t in _tl -%}
|
| 141 |
+
{%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
|
| 142 |
+
{{- tool_to_json(t) -}}
|
| 143 |
+
{%- endfor -%}
|
| 144 |
+
{%- endif -%}
|
| 145 |
+
{{- "\n</additional_tools>" -}}
|
| 146 |
+
{%- else -%}
|
| 147 |
+
{{- parse_content(item) -}}
|
| 148 |
+
{%- endif -%}
|
| 149 |
+
{%- endfor -%}
|
| 150 |
+
{%- endif -%}
|
| 151 |
+
{{- "\n</tool_response>" -}}
|
| 152 |
+
{%- endif -%}
|
| 153 |
+
{%- endmacro -%}
|
| 154 |
+
|
| 155 |
+
{%- set ns = namespace(system_prompt="", last_user_index=-1) -%}
|
| 156 |
+
{%- if messages and messages[0]["role"] == "system" -%}
|
| 157 |
+
{%- if messages[0].get("content") -%}
|
| 158 |
+
{%- set ns.system_prompt = parse_content(messages[0]["content"]) -%}
|
| 159 |
+
{%- endif -%}
|
| 160 |
+
{%- set messages = messages[1:] -%}
|
| 161 |
+
{%- endif -%}
|
| 162 |
+
{%- if tools -%}
|
| 163 |
+
{%- set tools_ns = namespace(lines=[]) -%}
|
| 164 |
+
{%- for tool in tools -%}
|
| 165 |
+
{%- set fn = tool.function if tool.function is defined else tool -%}
|
| 166 |
+
{%- set _dyn = (fn.defer_loading is defined and fn.defer_loading) or (tool.defer_loading is defined and tool.defer_loading) -%}
|
| 167 |
+
{%- if not _dyn -%}
|
| 168 |
+
{%- set tools_ns.lines = tools_ns.lines + [tool_to_json(fn)] -%}
|
| 169 |
+
{%- endif -%}
|
| 170 |
+
{%- endfor -%}
|
| 171 |
+
{%- if tools_ns.lines -%}
|
| 172 |
+
{%- set ns.system_prompt = ns.system_prompt + ("\n\n" if ns.system_prompt else "") + "<tools>\n" + (tools_ns.lines | join("\n")) + "\n</tools>" -%}
|
| 173 |
+
{%- endif -%}
|
| 174 |
+
{%- endif -%}
|
| 175 |
+
{%- if ns.system_prompt -%}
|
| 176 |
+
{{- "<|im_start|>system\n" + ns.system_prompt + "<|im_end|>\n" -}}
|
| 177 |
+
{%- endif -%}
|
| 178 |
+
{%- for message in messages -%}
|
| 179 |
+
{%- if message["role"] == "user" -%}
|
| 180 |
+
{%- set ns.last_user_index = loop.index0 -%}
|
| 181 |
+
{%- endif -%}
|
| 182 |
+
{%- endfor -%}
|
| 183 |
+
{#- vLLM's and SGLang's Anthropic /v1/messages adapters split one tool_result that carries
|
| 184 |
+
tool_reference blocks into consecutive tool messages with the same tool_call_id (text first,
|
| 185 |
+
references second). Merge such runs into one tool turn so they render exactly like a single
|
| 186 |
+
OpenAI-format tool message whose content holds the same items. -#}
|
| 187 |
+
{%- set carry = namespace(items=[]) -%}
|
| 188 |
+
{%- for message in messages -%}
|
| 189 |
+
{%- set _next = messages[loop.index0 + 1] if not loop.last else none -%}
|
| 190 |
+
{%- set _merge = message.role == "tool" and not message.get("additional_tools")
|
| 191 |
+
and _next is not none and _next.role == "tool" and not _next.get("additional_tools")
|
| 192 |
+
and message.get("tool_call_id") and _next.get("tool_call_id") == message.get("tool_call_id") -%}
|
| 193 |
+
{%- set _items = [] -%}
|
| 194 |
+
{%- if message.role == "tool" and not message.get("additional_tools") -%}
|
| 195 |
+
{%- set _c = message.get("content") -%}
|
| 196 |
+
{%- if _c is string -%}
|
| 197 |
+
{%- set _items = [{"type": "text", "text": _c}] if _c else [] -%}
|
| 198 |
+
{%- elif _c is mapping -%}
|
| 199 |
+
{%- set _items = [_c] -%}
|
| 200 |
+
{%- elif _c -%}
|
| 201 |
+
{%- set _items = _c | list -%}
|
| 202 |
+
{%- endif -%}
|
| 203 |
+
{%- endif -%}
|
| 204 |
+
{%- if _merge -%}
|
| 205 |
+
{%- set carry.items = carry.items + _items -%}
|
| 206 |
+
{%- else -%}
|
| 207 |
+
{{- "<|im_start|>" + message.role + "\n" -}}
|
| 208 |
+
{%- if message.role == "assistant" -%}
|
| 209 |
+
{%- generation -%}
|
| 210 |
+
{%- set thinking = message.thinking if message.thinking is defined else message.reasoning if message.reasoning is defined else message.reasoning_content -%}
|
| 211 |
+
{%- set keep_thinking = preserve_thinking or loop.index0 > ns.last_user_index -%}
|
| 212 |
+
{%- if thinking is defined and keep_thinking -%}
|
| 213 |
+
{{- "<think>" + (thinking or "") + "</think>" -}}
|
| 214 |
+
{%- elif keep_thinking and "</think>" not in (message.get("content") if message.get("content") is string else "") -%}
|
| 215 |
+
{{- "<think></think>" -}}
|
| 216 |
+
{%- endif -%}
|
| 217 |
+
{%- set _cfm_tag = "CONTINUE_FINAL_MESSAGE_TAG " -%}
|
| 218 |
+
{%- set _has_cfm = false -%}
|
| 219 |
+
{%- set content = "" -%}
|
| 220 |
+
{%- if message.get("content") -%}
|
| 221 |
+
{%- set content = parse_content(message.content) -%}
|
| 222 |
+
{%- endif -%}
|
| 223 |
+
{%- if not keep_thinking -%}
|
| 224 |
+
{%- if "</think>" in content -%}
|
| 225 |
+
{%- set content = content.split("</think>")[-1] | trim -%}
|
| 226 |
+
{%- endif -%}
|
| 227 |
+
{%- set content = "<think></think>" + content -%}
|
| 228 |
+
{%- endif -%}
|
| 229 |
+
{%- if content.endswith(_cfm_tag) -%}
|
| 230 |
+
{%- set _has_cfm = true -%}
|
| 231 |
+
{%- set _trunc_len = (content | length) - (_cfm_tag | length) -%}
|
| 232 |
+
{%- set content = content[:_trunc_len] -%}
|
| 233 |
+
{%- endif -%}
|
| 234 |
+
{{- content -}}
|
| 235 |
+
{%- if message.tool_calls -%}
|
| 236 |
+
{{- render_tool_calls(message.tool_calls) -}}
|
| 237 |
+
{%- endif -%}
|
| 238 |
+
{%- if _has_cfm -%}
|
| 239 |
+
{{- _cfm_tag -}}
|
| 240 |
+
{%- endif -%}
|
| 241 |
+
{{- "<|im_end|>\n" -}}
|
| 242 |
+
{%- endgeneration -%}
|
| 243 |
+
{%- elif message.role == "tool" -%}
|
| 244 |
+
{%- if message.get("additional_tools") -%}
|
| 245 |
+
{%- set _at = message["additional_tools"] -%}
|
| 246 |
+
{#- definitions may live in additional_tools itself, or in content
|
| 247 |
+
with additional_tools used as a plain truthy flag -#}
|
| 248 |
+
{%- if _at is mapping or (_at is sequence and _at is not string) -%}
|
| 249 |
+
{%- set _src = _at -%}
|
| 250 |
+
{%- else -%}
|
| 251 |
+
{%- set _src = message.get("content") -%}
|
| 252 |
+
{%- endif -%}
|
| 253 |
+
{#- nothing to reveal: skip the wrapper entirely rather than
|
| 254 |
+
emitting an empty <additional_tools> block -#}
|
| 255 |
+
{%- if _src is string and "<tool_response>" in _src -%}
|
| 256 |
+
{{- _src -}}
|
| 257 |
+
{%- elif _src is string and "<additional_tools>" in _src -%}
|
| 258 |
+
{{- "<tool_response>\n" + _src + "\n</tool_response>" -}}
|
| 259 |
+
{%- elif _src -%}
|
| 260 |
+
{{- "<tool_response>\n<additional_tools>\n" -}}
|
| 261 |
+
{%- if _src is string -%}
|
| 262 |
+
{{- _src -}}
|
| 263 |
+
{%- elif _src is mapping -%}
|
| 264 |
+
{{- tool_to_json(_src) -}}
|
| 265 |
+
{%- else -%}
|
| 266 |
+
{%- for t in _src -%}
|
| 267 |
+
{%- if not loop.first -%}{{- "\n" -}}{%- endif -%}
|
| 268 |
+
{{- tool_to_json(t) -}}
|
| 269 |
+
{%- endfor -%}
|
| 270 |
+
{%- endif -%}
|
| 271 |
+
{{- "\n</additional_tools>\n</tool_response>" -}}
|
| 272 |
+
{%- endif -%}
|
| 273 |
+
{%- elif carry.items -%}
|
| 274 |
+
{{- render_tool_response(carry.items + _items) -}}
|
| 275 |
+
{%- set carry.items = [] -%}
|
| 276 |
+
{%- else -%}
|
| 277 |
+
{#- an empty or missing result still gets the wrapper, so every tool turn has the same shape -#}
|
| 278 |
+
{{- render_tool_response(message.get("content") or "") -}}
|
| 279 |
+
{%- endif -%}
|
| 280 |
+
{{- "<|im_end|>\n" -}}
|
| 281 |
+
{%- else %}
|
| 282 |
+
{%- if message.get("content") -%}
|
| 283 |
+
{{- parse_content(message["content"]) -}}
|
| 284 |
+
{%- endif -%}
|
| 285 |
+
{{- "<|im_end|>\n" -}}
|
| 286 |
+
{%- endif %}
|
| 287 |
+
{%- endif -%}
|
| 288 |
+
{%- endfor -%}
|
| 289 |
+
{%- if add_generation_prompt -%}
|
| 290 |
+
{{- "<|im_start|>assistant\n" -}}
|
| 291 |
+
{%- if enable_thinking -%}
|
| 292 |
+
{{- "<think>" -}}
|
| 293 |
+
{%- else -%}
|
| 294 |
+
{{- "<think></think>" -}}
|
| 295 |
+
{%- endif -%}
|
| 296 |
+
{%- endif -%}
|
config.json
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Lfm3OmniForConditionalGeneration"
|
| 4 |
+
],
|
| 5 |
+
"audio_encoder_config": {
|
| 6 |
+
"_name_or_path": "",
|
| 7 |
+
"architectures": null,
|
| 8 |
+
"chunk_size_feed_forward": 0,
|
| 9 |
+
"conv_channels": 8,
|
| 10 |
+
"dtype": null,
|
| 11 |
+
"hidden_size": 16,
|
| 12 |
+
"id2label": {
|
| 13 |
+
"0": "LABEL_0",
|
| 14 |
+
"1": "LABEL_1"
|
| 15 |
+
},
|
| 16 |
+
"initializer_range": 0.02,
|
| 17 |
+
"is_encoder_decoder": false,
|
| 18 |
+
"label2id": {
|
| 19 |
+
"LABEL_0": 0,
|
| 20 |
+
"LABEL_1": 1
|
| 21 |
+
},
|
| 22 |
+
"model_type": "lfm3_omni_encoder",
|
| 23 |
+
"num_mel_bins": 128,
|
| 24 |
+
"output_attentions": false,
|
| 25 |
+
"output_hidden_states": false,
|
| 26 |
+
"problem_type": null,
|
| 27 |
+
"return_dict": true,
|
| 28 |
+
"sliding_window": 4,
|
| 29 |
+
"text_config": {
|
| 30 |
+
"_name_or_path": "",
|
| 31 |
+
"architectures": null,
|
| 32 |
+
"block_auto_adjust_ff_dim": false,
|
| 33 |
+
"block_ffn_dim_multiplier": 1.0,
|
| 34 |
+
"block_multiple_of": 256,
|
| 35 |
+
"bos_token_id": 1,
|
| 36 |
+
"chunk_size_feed_forward": 0,
|
| 37 |
+
"conv_L_cache": 3,
|
| 38 |
+
"conv_bias": false,
|
| 39 |
+
"dtype": null,
|
| 40 |
+
"eos_token_id": null,
|
| 41 |
+
"full_attn_idxs": null,
|
| 42 |
+
"hidden_size": 16,
|
| 43 |
+
"id2label": {
|
| 44 |
+
"0": "LABEL_0",
|
| 45 |
+
"1": "LABEL_1"
|
| 46 |
+
},
|
| 47 |
+
"initializer_range": 0.02,
|
| 48 |
+
"intermediate_size": 32,
|
| 49 |
+
"is_encoder_decoder": false,
|
| 50 |
+
"label2id": {
|
| 51 |
+
"LABEL_0": 0,
|
| 52 |
+
"LABEL_1": 1
|
| 53 |
+
},
|
| 54 |
+
"layer_types": [
|
| 55 |
+
"conv",
|
| 56 |
+
"full_attention",
|
| 57 |
+
"conv",
|
| 58 |
+
"full_attention"
|
| 59 |
+
],
|
| 60 |
+
"max_position_embeddings": 128000,
|
| 61 |
+
"model_type": "lfm2",
|
| 62 |
+
"norm_eps": 1e-05,
|
| 63 |
+
"num_attention_heads": 2,
|
| 64 |
+
"num_hidden_layers": 4,
|
| 65 |
+
"num_key_value_heads": 1,
|
| 66 |
+
"output_attentions": false,
|
| 67 |
+
"output_hidden_states": false,
|
| 68 |
+
"pad_token_id": 0,
|
| 69 |
+
"problem_type": null,
|
| 70 |
+
"return_dict": true,
|
| 71 |
+
"rope_parameters": {
|
| 72 |
+
"rope_theta": 1000000.0,
|
| 73 |
+
"rope_type": "default"
|
| 74 |
+
},
|
| 75 |
+
"tie_word_embeddings": true,
|
| 76 |
+
"use_cache": true,
|
| 77 |
+
"vocab_size": 2
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"audio_eos_code": 2016,
|
| 81 |
+
"audio_loss_weight": 1.0,
|
| 82 |
+
"audio_out_token_id": 125032,
|
| 83 |
+
"audio_start_token_id": 125012,
|
| 84 |
+
"audio_token_id": 125031,
|
| 85 |
+
"auto_map": {
|
| 86 |
+
"AutoConfig": "configuration_lfm3_omni.Lfm3OmniConfig",
|
| 87 |
+
"AutoModel": "modeling_lfm3_omni.Lfm3OmniModel",
|
| 88 |
+
"AutoModelForImageTextToText": "modeling_lfm3_omni.Lfm3OmniForConditionalGeneration",
|
| 89 |
+
"AutoModelForMultimodalLM": "modeling_lfm3_omni.Lfm3OmniForConditionalGeneration",
|
| 90 |
+
"AutoModelForSeq2SeqLM": "modeling_lfm3_omni.Lfm3OmniForConditionalGeneration"
|
| 91 |
+
},
|
| 92 |
+
"codebook_size": 2017,
|
| 93 |
+
"depth_config": {
|
| 94 |
+
"_name_or_path": "",
|
| 95 |
+
"architectures": null,
|
| 96 |
+
"bos_token_id": null,
|
| 97 |
+
"chunk_size_feed_forward": 0,
|
| 98 |
+
"conv_L_cache": 7,
|
| 99 |
+
"conv_bias": false,
|
| 100 |
+
"dtype": null,
|
| 101 |
+
"eos_token_id": null,
|
| 102 |
+
"head_dim": 4,
|
| 103 |
+
"hidden_size": 16,
|
| 104 |
+
"id2label": {
|
| 105 |
+
"0": "LABEL_0",
|
| 106 |
+
"1": "LABEL_1"
|
| 107 |
+
},
|
| 108 |
+
"initializer_range": 0.02,
|
| 109 |
+
"intermediate_size": 32,
|
| 110 |
+
"is_encoder_decoder": false,
|
| 111 |
+
"label2id": {
|
| 112 |
+
"LABEL_0": 0,
|
| 113 |
+
"LABEL_1": 1
|
| 114 |
+
},
|
| 115 |
+
"layer_types": [
|
| 116 |
+
"full_attention",
|
| 117 |
+
"full_attention"
|
| 118 |
+
],
|
| 119 |
+
"max_position_embeddings": 12,
|
| 120 |
+
"model_type": "lfm3",
|
| 121 |
+
"norm_eps": 1e-05,
|
| 122 |
+
"num_attention_heads": 4,
|
| 123 |
+
"num_hidden_layers": 2,
|
| 124 |
+
"num_key_value_heads": 2,
|
| 125 |
+
"output_attentions": false,
|
| 126 |
+
"output_hidden_states": false,
|
| 127 |
+
"pad_token_id": null,
|
| 128 |
+
"problem_type": null,
|
| 129 |
+
"return_dict": true,
|
| 130 |
+
"rope_parameters": {
|
| 131 |
+
"full_attention": {
|
| 132 |
+
"partial_rotary_factor": 1.0,
|
| 133 |
+
"rope_theta": 1000000.0,
|
| 134 |
+
"rope_type": "default"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"sliding_window": 256,
|
| 138 |
+
"tie_word_embeddings": true,
|
| 139 |
+
"use_cache": false,
|
| 140 |
+
"use_sandwich_norm": false,
|
| 141 |
+
"vocab_size": 1
|
| 142 |
+
},
|
| 143 |
+
"downsample_factor": 2,
|
| 144 |
+
"dtype": "float32",
|
| 145 |
+
"image_token_id": 125030,
|
| 146 |
+
"initializer_range": 0.02,
|
| 147 |
+
"interleaved_n_audio": 12,
|
| 148 |
+
"interleaved_n_text": 6,
|
| 149 |
+
"maskgit_schedule": [
|
| 150 |
+
4,
|
| 151 |
+
4,
|
| 152 |
+
4
|
| 153 |
+
],
|
| 154 |
+
"mixed_start_token_id": 125015,
|
| 155 |
+
"model_type": "lfm3_omni",
|
| 156 |
+
"num_codebooks": 12,
|
| 157 |
+
"projector_bias": true,
|
| 158 |
+
"projector_hidden_act": "gelu",
|
| 159 |
+
"projector_hidden_size": 32,
|
| 160 |
+
"projector_layer_norm_eps": 1e-06,
|
| 161 |
+
"text_config": {
|
| 162 |
+
"_name_or_path": "",
|
| 163 |
+
"architectures": null,
|
| 164 |
+
"bos_token_id": 124894,
|
| 165 |
+
"chunk_size_feed_forward": 0,
|
| 166 |
+
"conv_L_cache": 7,
|
| 167 |
+
"conv_bias": false,
|
| 168 |
+
"dtype": null,
|
| 169 |
+
"eos_token_id": 124900,
|
| 170 |
+
"head_dim": 8,
|
| 171 |
+
"hidden_size": 16,
|
| 172 |
+
"id2label": {
|
| 173 |
+
"0": "LABEL_0",
|
| 174 |
+
"1": "LABEL_1"
|
| 175 |
+
},
|
| 176 |
+
"initializer_range": 0.02,
|
| 177 |
+
"intermediate_size": 32,
|
| 178 |
+
"is_encoder_decoder": false,
|
| 179 |
+
"label2id": {
|
| 180 |
+
"LABEL_0": 0,
|
| 181 |
+
"LABEL_1": 1
|
| 182 |
+
},
|
| 183 |
+
"layer_types": [
|
| 184 |
+
"conv",
|
| 185 |
+
"conv",
|
| 186 |
+
"full_attention",
|
| 187 |
+
"conv",
|
| 188 |
+
"sliding_attention",
|
| 189 |
+
"conv",
|
| 190 |
+
"sliding_attention"
|
| 191 |
+
],
|
| 192 |
+
"max_position_embeddings": 128000,
|
| 193 |
+
"model_type": "lfm3",
|
| 194 |
+
"norm_eps": 1e-05,
|
| 195 |
+
"num_attention_heads": 4,
|
| 196 |
+
"num_hidden_layers": 7,
|
| 197 |
+
"num_key_value_heads": 2,
|
| 198 |
+
"output_attentions": false,
|
| 199 |
+
"output_hidden_states": false,
|
| 200 |
+
"pad_token_id": 124893,
|
| 201 |
+
"problem_type": null,
|
| 202 |
+
"return_dict": true,
|
| 203 |
+
"rope_parameters": {
|
| 204 |
+
"full_attention": {
|
| 205 |
+
"partial_rotary_factor": 0.5,
|
| 206 |
+
"rope_theta": 1000000.0,
|
| 207 |
+
"rope_type": "default"
|
| 208 |
+
},
|
| 209 |
+
"sliding_attention": {
|
| 210 |
+
"rope_theta": 10000,
|
| 211 |
+
"rope_type": "default"
|
| 212 |
+
}
|
| 213 |
+
},
|
| 214 |
+
"sliding_window": 4,
|
| 215 |
+
"tie_word_embeddings": true,
|
| 216 |
+
"use_cache": true,
|
| 217 |
+
"use_sandwich_norm": true,
|
| 218 |
+
"vocab_size": 128000
|
| 219 |
+
},
|
| 220 |
+
"text_end_token_id": 125014,
|
| 221 |
+
"text_loss_weight": 1.0,
|
| 222 |
+
"tie_word_embeddings": true,
|
| 223 |
+
"transformers_version": "5.20.0",
|
| 224 |
+
"video_token_id": 125029,
|
| 225 |
+
"vision_config": {
|
| 226 |
+
"_name_or_path": "",
|
| 227 |
+
"architectures": null,
|
| 228 |
+
"chunk_size_feed_forward": 0,
|
| 229 |
+
"conv_kernel_size": 3,
|
| 230 |
+
"dtype": "float32",
|
| 231 |
+
"hidden_size": 16,
|
| 232 |
+
"id2label": {
|
| 233 |
+
"0": "LABEL_0",
|
| 234 |
+
"1": "LABEL_1"
|
| 235 |
+
},
|
| 236 |
+
"in_channels": 3,
|
| 237 |
+
"initializer_range": 0.02,
|
| 238 |
+
"intermediate_size": 32,
|
| 239 |
+
"is_encoder_decoder": false,
|
| 240 |
+
"label2id": {
|
| 241 |
+
"LABEL_0": 0,
|
| 242 |
+
"LABEL_1": 1
|
| 243 |
+
},
|
| 244 |
+
"layer_types": [
|
| 245 |
+
"conv",
|
| 246 |
+
"full_attention",
|
| 247 |
+
"conv",
|
| 248 |
+
"full_attention"
|
| 249 |
+
],
|
| 250 |
+
"model_type": "lfm3_vl_vision",
|
| 251 |
+
"norm_eps": 1e-05,
|
| 252 |
+
"num_attention_heads": 2,
|
| 253 |
+
"num_hidden_layers": 4,
|
| 254 |
+
"num_key_value_heads": 1,
|
| 255 |
+
"output_attentions": false,
|
| 256 |
+
"output_hidden_states": false,
|
| 257 |
+
"patch_size": 16,
|
| 258 |
+
"problem_type": null,
|
| 259 |
+
"return_dict": true,
|
| 260 |
+
"rope_parameters": {
|
| 261 |
+
"rope_theta": 1000000.0,
|
| 262 |
+
"rope_type": "axial"
|
| 263 |
+
},
|
| 264 |
+
"spatial_merge_size": 2,
|
| 265 |
+
"temporal_patch_size": 2
|
| 266 |
+
}
|
| 267 |
+
}
|
configuration_lfm3.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
from huggingface_hub.dataclasses import strict
|
| 15 |
+
|
| 16 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 17 |
+
from transformers.utils import auto_docstring
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@auto_docstring(checkpoint="LiquidAI/LFM3-1.7B")
|
| 21 |
+
@strict
|
| 22 |
+
class Lfm3Config(PreTrainedConfig):
|
| 23 |
+
r"""
|
| 24 |
+
rope_parameters (`dict[str, RopeParameters]`, *optional*):
|
| 25 |
+
RoPE parameters keyed by attention layer type, for example `{"full_attention": {"rope_type": "default",
|
| 26 |
+
"rope_theta": 1000000.0, "partial_rotary_factor": 0.5}, "sliding_attention": {"rope_type": "default",
|
| 27 |
+
"rope_theta": 10000.0}}`. `conv` layers do not use RoPE and have no entry. Full-attention layers rotate the
|
| 28 |
+
first `partial_rotary_factor * head_dim` dimensions and leave the rest unrotated.
|
| 29 |
+
conv_bias (`bool`, *optional*, defaults to `False`):
|
| 30 |
+
Whether to use bias in the conv layers.
|
| 31 |
+
conv_L_cache (`int`, *optional*, defaults to 7):
|
| 32 |
+
L_cache dim in the conv layers.
|
| 33 |
+
layer_types (`list[str]`, *optional*):
|
| 34 |
+
Per-layer operator type, each one of `"full_attention"`, `"sliding_attention"` or `"conv"`. Defaults to the
|
| 35 |
+
LFM3 pattern of a full-attention layer every fourth layer starting from the third, a sliding-attention layer
|
| 36 |
+
in between, and gated short convolutions elsewhere.
|
| 37 |
+
sliding_window (`int`, *optional*, defaults to 256):
|
| 38 |
+
Sliding-window size of the `sliding_attention` layers.
|
| 39 |
+
use_sandwich_norm (`bool`, *optional*, defaults to `True`):
|
| 40 |
+
Whether to apply an RMSNorm to the operator and feed-forward outputs before they enter the residual stream.
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
>>> from transformers import Lfm3Model, Lfm3Config
|
| 44 |
+
|
| 45 |
+
>>> # Initializing a LFM3 model
|
| 46 |
+
>>> configuration = Lfm3Config()
|
| 47 |
+
|
| 48 |
+
>>> # Initializing a model from the LFM3-1.7B style configuration
|
| 49 |
+
>>> model = Lfm3Model(configuration)
|
| 50 |
+
|
| 51 |
+
>>> # Accessing the model configuration
|
| 52 |
+
>>> configuration = model.config
|
| 53 |
+
```
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
model_type = "lfm3"
|
| 57 |
+
keys_to_ignore_at_inference = ["past_key_values"]
|
| 58 |
+
default_theta = 1000000.0
|
| 59 |
+
|
| 60 |
+
vocab_size: int = 128_000
|
| 61 |
+
hidden_size: int = 2048
|
| 62 |
+
intermediate_size: int = 7680
|
| 63 |
+
num_hidden_layers: int = 24
|
| 64 |
+
num_attention_heads: int = 16
|
| 65 |
+
num_key_value_heads: int = 4
|
| 66 |
+
max_position_embeddings: int = 128_000
|
| 67 |
+
initializer_range: float = 0.02
|
| 68 |
+
norm_eps: float = 0.00001
|
| 69 |
+
use_cache: bool = True
|
| 70 |
+
pad_token_id: int | None = 124_893
|
| 71 |
+
bos_token_id: int | None = 124_894
|
| 72 |
+
eos_token_id: int | list[int] | None = 124_900
|
| 73 |
+
tie_word_embeddings: bool = True
|
| 74 |
+
rope_parameters: dict | None = None
|
| 75 |
+
conv_bias: bool = False
|
| 76 |
+
conv_L_cache: int = 7
|
| 77 |
+
layer_types: list[str] | None = None
|
| 78 |
+
_attention_layer_types = ("full_attention", "sliding_attention")
|
| 79 |
+
head_dim: int | None = None
|
| 80 |
+
sliding_window: int | None = 256
|
| 81 |
+
use_sandwich_norm: bool = True
|
| 82 |
+
|
| 83 |
+
def __post_init__(self, **kwargs):
|
| 84 |
+
if self.head_dim is None:
|
| 85 |
+
self.head_dim = self.hidden_size // self.num_attention_heads
|
| 86 |
+
if self.rope_parameters is None:
|
| 87 |
+
self.rope_parameters = {
|
| 88 |
+
"full_attention": {"rope_type": "default", "rope_theta": 1_000_000.0, "partial_rotary_factor": 0.5},
|
| 89 |
+
"sliding_attention": {"rope_type": "default", "rope_theta": 10_000.0},
|
| 90 |
+
}
|
| 91 |
+
if self.layer_types is None:
|
| 92 |
+
self.layer_types = [
|
| 93 |
+
"full_attention" if i % 4 == 2 else "sliding_attention" if i % 4 == 0 and i > 0 else "conv"
|
| 94 |
+
for i in range(self.num_hidden_layers)
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
self.tie_word_embeddings = kwargs.pop("tie_embedding", self.tie_word_embeddings)
|
| 98 |
+
super().__post_init__(**kwargs)
|
| 99 |
+
|
| 100 |
+
def validate_architecture(self):
|
| 101 |
+
"""Part of ``@strict``-powered validation."""
|
| 102 |
+
if len(self.layer_types) != self.num_hidden_layers:
|
| 103 |
+
raise ValueError(
|
| 104 |
+
f"`layer_types` has {len(self.layer_types)} entries but `num_hidden_layers` is {self.num_hidden_layers}."
|
| 105 |
+
)
|
| 106 |
+
allowed_layer_types = (*self._attention_layer_types, "conv")
|
| 107 |
+
invalid_layer_types = [layer_type for layer_type in self.layer_types if layer_type not in allowed_layer_types]
|
| 108 |
+
if invalid_layer_types:
|
| 109 |
+
raise ValueError(f"`layer_types` entries must be one of {allowed_layer_types}; got {invalid_layer_types}.")
|
| 110 |
+
if "sliding_attention" in self.layer_types and self.sliding_window is None:
|
| 111 |
+
raise ValueError("`layer_types` has `sliding_attention` layers but `sliding_window` is None.")
|
| 112 |
+
missing_rope = [
|
| 113 |
+
layer_type
|
| 114 |
+
for layer_type in self._attention_layer_types
|
| 115 |
+
if layer_type in self.layer_types and layer_type not in self.rope_parameters
|
| 116 |
+
]
|
| 117 |
+
if missing_rope:
|
| 118 |
+
raise ValueError(f"`rope_parameters` has no entry for the {missing_rope} layers in `layer_types`.")
|
| 119 |
+
for layer_type, rope_params in self.rope_parameters.items():
|
| 120 |
+
if not isinstance(rope_params, dict):
|
| 121 |
+
continue
|
| 122 |
+
if (
|
| 123 |
+
rope_params.get("rope_type", "default") in ("dynamic", "longrope")
|
| 124 |
+
and rope_params.get("partial_rotary_factor", 1.0) < 1
|
| 125 |
+
):
|
| 126 |
+
raise ValueError(
|
| 127 |
+
f"`{rope_params['rope_type']}` RoPE recomputes its frequencies during the forward pass over the "
|
| 128 |
+
f"rotated dimension only and cannot keep LFM3's partial rotation on `{layer_type}` layers."
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
__all__ = ["Lfm3Config"]
|
configuration_lfm3_omni.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
from huggingface_hub.dataclasses import strict
|
| 15 |
+
|
| 16 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 17 |
+
from transformers.models.auto import CONFIG_MAPPING, AutoConfig
|
| 18 |
+
from transformers.utils import auto_docstring
|
| 19 |
+
|
| 20 |
+
from .configuration_lfm3 import Lfm3Config
|
| 21 |
+
from .configuration_lfm3_vl import Lfm3VlVisionConfig
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _config_class(model_type):
|
| 25 |
+
# the LFM3 configs ship with the checkpoint instead of the transformers registry
|
| 26 |
+
remote = {"lfm3": Lfm3Config, "lfm3_vl_vision": Lfm3VlVisionConfig, "lfm3_omni_encoder": Lfm3OmniEncoderConfig}
|
| 27 |
+
return remote.get(model_type) or CONFIG_MAPPING[model_type]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# `dtype` is dropped so that the top-level dtype governs every component of a legacy export
|
| 31 |
+
_LEGACY_SUBCONFIG_DROPPED_KEYS = (
|
| 32 |
+
"architectures",
|
| 33 |
+
"transformers_version",
|
| 34 |
+
"is_causal",
|
| 35 |
+
"auto_map",
|
| 36 |
+
"_name_or_path",
|
| 37 |
+
"dtype",
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _subconfig_dict(values: dict, default_model_type: str) -> dict:
|
| 42 |
+
values = {key: value for key, value in values.items() if key not in _LEGACY_SUBCONFIG_DROPPED_KEYS}
|
| 43 |
+
values.setdefault("model_type", default_model_type)
|
| 44 |
+
return values
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@auto_docstring(checkpoint="LiquidAI/lfm3_omni_unstable")
|
| 48 |
+
@strict
|
| 49 |
+
class Lfm3OmniEncoderConfig(PreTrainedConfig):
|
| 50 |
+
r"""
|
| 51 |
+
text_config (`Lfm2Config` or `dict`, *optional*):
|
| 52 |
+
The configuration of the LFM2 model run over the downsampled spectrogram. Defaults to the 17-layer,
|
| 53 |
+
768-wide model of LFM3-Audio.
|
| 54 |
+
num_mel_bins (`int`, *optional*, defaults to 128):
|
| 55 |
+
The number of mel bins of the input features.
|
| 56 |
+
conv_channels (`int`, *optional*, defaults to 256):
|
| 57 |
+
The number of channels of the downsampling convolutions.
|
| 58 |
+
hidden_size (`int`, *optional*, defaults to 768):
|
| 59 |
+
The width of the encoder output, equal to `text_config.hidden_size`.
|
| 60 |
+
sliding_window (`int`, *optional*, defaults to 32):
|
| 61 |
+
The number of past frames each frame attends to, itself included.
|
| 62 |
+
initializer_range (`float`, *optional*, defaults to 0.02):
|
| 63 |
+
The standard deviation of the truncated normal initializer of the convolutions.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
model_type = "lfm3_omni_encoder"
|
| 67 |
+
sub_configs = {"text_config": AutoConfig}
|
| 68 |
+
|
| 69 |
+
text_config: dict | PreTrainedConfig | None = None
|
| 70 |
+
num_mel_bins: int = 128
|
| 71 |
+
conv_channels: int = 256
|
| 72 |
+
hidden_size: int = 768
|
| 73 |
+
sliding_window: int = 32
|
| 74 |
+
initializer_range: float = 0.02
|
| 75 |
+
|
| 76 |
+
def __post_init__(self, **kwargs):
|
| 77 |
+
if isinstance(self.text_config, dict):
|
| 78 |
+
# the encoder consumes embeddings only: no end-of-sequence id in its two-token vocabulary
|
| 79 |
+
self.text_config = {"eos_token_id": None, **_subconfig_dict(self.text_config, "lfm2")}
|
| 80 |
+
self.text_config = _config_class(self.text_config["model_type"])(**self.text_config)
|
| 81 |
+
elif self.text_config is None:
|
| 82 |
+
self.text_config = _config_class("lfm2")(
|
| 83 |
+
vocab_size=2,
|
| 84 |
+
hidden_size=self.hidden_size,
|
| 85 |
+
intermediate_size=2560,
|
| 86 |
+
num_hidden_layers=17,
|
| 87 |
+
num_attention_heads=8,
|
| 88 |
+
num_key_value_heads=4,
|
| 89 |
+
layer_types=(["conv", "conv", "full_attention"] * 5) + ["full_attention", "full_attention"],
|
| 90 |
+
conv_L_cache=3,
|
| 91 |
+
rope_parameters={"rope_type": "default", "rope_theta": 1_000_000.0},
|
| 92 |
+
pad_token_id=0,
|
| 93 |
+
bos_token_id=1,
|
| 94 |
+
eos_token_id=None,
|
| 95 |
+
)
|
| 96 |
+
super().__post_init__(**kwargs)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@auto_docstring(checkpoint="LiquidAI/LFM3-Omni-2.2B")
|
| 100 |
+
@strict
|
| 101 |
+
class Lfm3OmniConfig(PreTrainedConfig):
|
| 102 |
+
r"""
|
| 103 |
+
This is the configuration class to store the configuration of a [`Lfm3OmniForConditionalGeneration`].
|
| 104 |
+
|
| 105 |
+
text_config (`Lfm3Config` or `dict`, *optional*):
|
| 106 |
+
The configuration of the LFM3 language model.
|
| 107 |
+
audio_encoder_config (`Lfm3OmniEncoderConfig` or `dict`, *optional*):
|
| 108 |
+
The configuration of the speech encoder.
|
| 109 |
+
depth_config (`Lfm3Config` or `dict`, *optional*):
|
| 110 |
+
The configuration of the depth decoder, a bidirectional LFM3 model over the codebook axis of one speech
|
| 111 |
+
frame. Defaults to the 3-layer, 1024-wide model of LFM3-Audio.
|
| 112 |
+
num_codebooks (`int`, *optional*, defaults to 12):
|
| 113 |
+
The number of codebooks per speech frame.
|
| 114 |
+
codebook_size (`int`, *optional*, defaults to 2017):
|
| 115 |
+
The size of each codebook, the codec's codes plus the end-of-audio code.
|
| 116 |
+
audio_eos_code (`int`, *optional*, defaults to 2016):
|
| 117 |
+
The end-of-audio code. A frame holding it in every codebook ends a speech segment.
|
| 118 |
+
maskgit_schedule (`list[int]`, *optional*, defaults to `[4, 4, 4]`):
|
| 119 |
+
The number of codebooks revealed by each pass of the depth decoder when sampling a frame.
|
| 120 |
+
interleaved_n_text (`int`, *optional*, defaults to 6):
|
| 121 |
+
The number of text tokens per block of the interleaved generation mode.
|
| 122 |
+
interleaved_n_audio (`int`, *optional*, defaults to 12):
|
| 123 |
+
The number of speech frames per block of the interleaved generation mode.
|
| 124 |
+
audio_token_id (`int`, *optional*, defaults to 125031):
|
| 125 |
+
The id of the placeholder token standing for one encoded input speech frame.
|
| 126 |
+
audio_out_token_id (`int`, *optional*, defaults to 125032):
|
| 127 |
+
The id of the placeholder token standing for one output speech frame.
|
| 128 |
+
audio_start_token_id (`int`, *optional*, defaults to 125012):
|
| 129 |
+
The id of `<|audio_start|>`, which switches generation to speech until an end-of-audio frame.
|
| 130 |
+
text_end_token_id (`int`, *optional*, defaults to 125014):
|
| 131 |
+
The id of `<|text_end|>`, which ends the text of an interleaved answer.
|
| 132 |
+
mixed_start_token_id (`int`, *optional*, defaults to 125015):
|
| 133 |
+
The id of `<|mixed_start|>`, which switches generation to interleaved text and speech.
|
| 134 |
+
tie_word_embeddings (`bool`, *optional*, defaults to `True`):
|
| 135 |
+
Whether to tie the language model head to the input embeddings.
|
| 136 |
+
text_loss_weight (`float`, *optional*, defaults to 1.0):
|
| 137 |
+
The weight of the text loss in the training loss.
|
| 138 |
+
audio_loss_weight (`float`, *optional*, defaults to 1.0):
|
| 139 |
+
The weight of the speech loss in the training loss.
|
| 140 |
+
initializer_range (`float`, *optional*, defaults to 0.02):
|
| 141 |
+
The standard deviation of the truncated normal initializer of the projector and depth decoder.
|
| 142 |
+
|
| 143 |
+
vision_config (`Lfm3VlVisionConfig` or `dict`, *optional*):
|
| 144 |
+
The configuration of the image encoder.
|
| 145 |
+
video_token_id (`int`, *optional*, defaults to 125029):
|
| 146 |
+
The id of the placeholder token standing for one encoded video patch group.
|
| 147 |
+
image_token_id (`int`, *optional*, defaults to 125030):
|
| 148 |
+
The placeholder for one projected image token.
|
| 149 |
+
projector_hidden_size (`int`, *optional*, defaults to 4096):
|
| 150 |
+
The intermediate dimension of the image projector.
|
| 151 |
+
projector_hidden_act (`str`, *optional*, defaults to `"gelu"`):
|
| 152 |
+
The image projector activation.
|
| 153 |
+
projector_bias (`bool`, *optional*, defaults to `True`):
|
| 154 |
+
Whether the image projector linear layers have biases.
|
| 155 |
+
projector_layer_norm_eps (`float`, *optional*, defaults to 1e-06):
|
| 156 |
+
The epsilon of the image projector layer normalization.
|
| 157 |
+
downsample_factor (`int`, *optional*, defaults to 2):
|
| 158 |
+
The side length of each image patch block merged into one language token.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
model_type = "lfm3_omni"
|
| 162 |
+
sub_configs = {
|
| 163 |
+
"text_config": AutoConfig,
|
| 164 |
+
"audio_encoder_config": Lfm3OmniEncoderConfig,
|
| 165 |
+
"depth_config": AutoConfig,
|
| 166 |
+
"vision_config": AutoConfig,
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
text_config: dict | PreTrainedConfig | None = None
|
| 170 |
+
audio_encoder_config: dict | PreTrainedConfig | None = None
|
| 171 |
+
depth_config: dict | PreTrainedConfig | None = None
|
| 172 |
+
num_codebooks: int = 12
|
| 173 |
+
codebook_size: int = 2017
|
| 174 |
+
audio_eos_code: int = 2016
|
| 175 |
+
maskgit_schedule: list[int] | None = None
|
| 176 |
+
interleaved_n_text: int = 6
|
| 177 |
+
interleaved_n_audio: int = 12
|
| 178 |
+
audio_token_id: int = 125_031
|
| 179 |
+
audio_out_token_id: int = 125_032
|
| 180 |
+
audio_start_token_id: int = 125_012
|
| 181 |
+
text_end_token_id: int = 125_014
|
| 182 |
+
mixed_start_token_id: int = 125_015
|
| 183 |
+
tie_word_embeddings: bool = True
|
| 184 |
+
text_loss_weight: float = 1.0
|
| 185 |
+
audio_loss_weight: float = 1.0
|
| 186 |
+
initializer_range: float = 0.02
|
| 187 |
+
|
| 188 |
+
# the ids `generate` and `transcribe_live` index the text logits with
|
| 189 |
+
_token_id_fields = (
|
| 190 |
+
"audio_token_id",
|
| 191 |
+
"audio_out_token_id",
|
| 192 |
+
"audio_start_token_id",
|
| 193 |
+
"text_end_token_id",
|
| 194 |
+
"mixed_start_token_id",
|
| 195 |
+
)
|
| 196 |
+
vision_config: dict | PreTrainedConfig | None = None
|
| 197 |
+
image_token_id: int = 125_030
|
| 198 |
+
video_token_id: int = 125_029
|
| 199 |
+
projector_hidden_size: int = 4096
|
| 200 |
+
projector_hidden_act: str = "gelu"
|
| 201 |
+
projector_bias: bool = True
|
| 202 |
+
projector_layer_norm_eps: float = 1e-6
|
| 203 |
+
downsample_factor: int = 2
|
| 204 |
+
|
| 205 |
+
def __post_init__(self, **kwargs):
|
| 206 |
+
if isinstance(self.vision_config, dict):
|
| 207 |
+
self.vision_config = _config_class("lfm3_vl_vision")(**self.vision_config)
|
| 208 |
+
elif self.vision_config is None:
|
| 209 |
+
self.vision_config = _config_class("lfm3_vl_vision")()
|
| 210 |
+
if "lfm" in kwargs or "format_version" in kwargs:
|
| 211 |
+
raise ValueError(
|
| 212 |
+
"LFM3-Omni reads the canonical checkpoint layout only; training exports (`lfm` / `format_version`) "
|
| 213 |
+
"are not supported, load a canonical Hugging Face checkpoint."
|
| 214 |
+
)
|
| 215 |
+
if isinstance(self.text_config, dict):
|
| 216 |
+
self.text_config = _subconfig_dict(self.text_config, "lfm3")
|
| 217 |
+
self.text_config = _config_class(self.text_config["model_type"])(**self.text_config)
|
| 218 |
+
elif self.text_config is None:
|
| 219 |
+
self.text_config = _config_class("lfm3")()
|
| 220 |
+
self.validate_vision_fields()
|
| 221 |
+
if isinstance(self.audio_encoder_config, dict):
|
| 222 |
+
self.audio_encoder_config = self.sub_configs["audio_encoder_config"](**self.audio_encoder_config)
|
| 223 |
+
elif self.audio_encoder_config is None:
|
| 224 |
+
self.audio_encoder_config = self.sub_configs["audio_encoder_config"]()
|
| 225 |
+
if isinstance(self.depth_config, dict):
|
| 226 |
+
# the depth decoder consumes embeddings only; its token ids are placeholders
|
| 227 |
+
self.depth_config = {
|
| 228 |
+
"pad_token_id": None,
|
| 229 |
+
"bos_token_id": None,
|
| 230 |
+
"eos_token_id": None,
|
| 231 |
+
**_subconfig_dict(self.depth_config, "lfm3"),
|
| 232 |
+
}
|
| 233 |
+
self.depth_config = _config_class(self.depth_config["model_type"])(**self.depth_config)
|
| 234 |
+
elif self.depth_config is None:
|
| 235 |
+
self.depth_config = _config_class("lfm3")(
|
| 236 |
+
vocab_size=1,
|
| 237 |
+
pad_token_id=None,
|
| 238 |
+
hidden_size=1024,
|
| 239 |
+
intermediate_size=2816,
|
| 240 |
+
num_hidden_layers=3,
|
| 241 |
+
num_attention_heads=32,
|
| 242 |
+
num_key_value_heads=8,
|
| 243 |
+
head_dim=32,
|
| 244 |
+
max_position_embeddings=self.num_codebooks,
|
| 245 |
+
layer_types=["full_attention"] * 3,
|
| 246 |
+
rope_parameters={
|
| 247 |
+
"full_attention": {"rope_type": "default", "rope_theta": 1_000_000.0, "partial_rotary_factor": 1.0}
|
| 248 |
+
},
|
| 249 |
+
sliding_window=None,
|
| 250 |
+
use_sandwich_norm=False,
|
| 251 |
+
use_cache=False,
|
| 252 |
+
bos_token_id=None,
|
| 253 |
+
eos_token_id=None,
|
| 254 |
+
)
|
| 255 |
+
steps = kwargs.pop("maskgit_steps", 3)
|
| 256 |
+
if self.maskgit_schedule is None:
|
| 257 |
+
if not 1 <= steps <= self.num_codebooks:
|
| 258 |
+
raise ValueError(
|
| 259 |
+
f"maskgit_steps must be between 1 and num_codebooks={self.num_codebooks}, got {steps}"
|
| 260 |
+
)
|
| 261 |
+
self.maskgit_schedule = [self.num_codebooks // steps] * steps
|
| 262 |
+
self.maskgit_schedule[-1] += self.num_codebooks - sum(self.maskgit_schedule)
|
| 263 |
+
self.maskgit_schedule = list(self.maskgit_schedule)
|
| 264 |
+
self._validate_speech_fields()
|
| 265 |
+
super().__post_init__(**kwargs)
|
| 266 |
+
|
| 267 |
+
def validate_speech_fields(self):
|
| 268 |
+
"""Part of `@strict`-powered validation, so `validate()` and `save_pretrained` refuse later edits too."""
|
| 269 |
+
self._validate_speech_fields()
|
| 270 |
+
|
| 271 |
+
def _validate_speech_fields(self):
|
| 272 |
+
if min(self.maskgit_schedule, default=0) < 1 or sum(self.maskgit_schedule) != self.num_codebooks:
|
| 273 |
+
raise ValueError(
|
| 274 |
+
f"maskgit_schedule {self.maskgit_schedule} must be positive counts summing to "
|
| 275 |
+
f"num_codebooks={self.num_codebooks}"
|
| 276 |
+
)
|
| 277 |
+
# the end-of-audio code indexes every codebook; a code outside them could never be sampled or embedded
|
| 278 |
+
if not 0 <= self.audio_eos_code < self.codebook_size:
|
| 279 |
+
raise ValueError(
|
| 280 |
+
f"audio_eos_code={self.audio_eos_code} must lie in [0, codebook_size={self.codebook_size})"
|
| 281 |
+
)
|
| 282 |
+
if self.interleaved_n_text < 1 or self.interleaved_n_audio < 1:
|
| 283 |
+
raise ValueError("interleaved_n_text and interleaved_n_audio must be positive")
|
| 284 |
+
vocab_size = self.text_config.vocab_size
|
| 285 |
+
for name in self._token_id_fields:
|
| 286 |
+
if not 0 <= getattr(self, name) < vocab_size:
|
| 287 |
+
raise ValueError(f"{name}={getattr(self, name)} must lie in [0, text_config.vocab_size={vocab_size})")
|
| 288 |
+
|
| 289 |
+
def validate_vision_fields(self):
|
| 290 |
+
"""Part of `@strict`-powered validation, so `validate()` and `save_pretrained` refuse later edits too."""
|
| 291 |
+
if self.downsample_factor != self.vision_config.spatial_merge_size:
|
| 292 |
+
raise ValueError("downsample_factor must match vision_config.spatial_merge_size")
|
| 293 |
+
# an `inputs_embeds` forward embeds both placeholder ids to find them
|
| 294 |
+
vocab_size = self.text_config.vocab_size
|
| 295 |
+
for name in ("image_token_id", "video_token_id"):
|
| 296 |
+
if not 0 <= getattr(self, name) < vocab_size:
|
| 297 |
+
raise ValueError(f"{name}={getattr(self, name)} must lie in [0, text_config.vocab_size={vocab_size})")
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
__all__ = ["Lfm3OmniConfig", "Lfm3OmniEncoderConfig"]
|
configuration_lfm3_vl.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
from huggingface_hub.dataclasses import strict
|
| 15 |
+
|
| 16 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 17 |
+
from transformers.models.auto import CONFIG_MAPPING, AutoConfig
|
| 18 |
+
from transformers.utils import auto_docstring
|
| 19 |
+
|
| 20 |
+
from .configuration_lfm3 import Lfm3Config
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _config_class(model_type):
|
| 24 |
+
# the LFM3 configs ship with the checkpoint instead of the transformers registry
|
| 25 |
+
remote = {"lfm3": Lfm3Config, "lfm3_vl_vision": Lfm3VlVisionConfig}
|
| 26 |
+
return remote.get(model_type) or CONFIG_MAPPING[model_type]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@auto_docstring(checkpoint="LiquidAI/LFM3-VL-1.9B")
|
| 30 |
+
@strict
|
| 31 |
+
class Lfm3VlVisionConfig(PreTrainedConfig):
|
| 32 |
+
r"""
|
| 33 |
+
layer_types (`list[str]`, *optional*):
|
| 34 |
+
Per-layer operator type, each one of `"full_attention"` or `"conv"`. Defaults to two gated convolution
|
| 35 |
+
layers followed by alternating attention and convolution layers.
|
| 36 |
+
patch_size (`int`, *optional*, defaults to 16):
|
| 37 |
+
The spatial size of each image patch.
|
| 38 |
+
temporal_patch_size (`int`, *optional*, defaults to 2):
|
| 39 |
+
The number of video frames packed into one patch.
|
| 40 |
+
spatial_merge_size (`int`, *optional*, defaults to 2):
|
| 41 |
+
The side of the square block of patches merged into one language token.
|
| 42 |
+
conv_kernel_size (`int`, *optional*, defaults to 3):
|
| 43 |
+
The kernel size of the depthwise 2D convolution layers.
|
| 44 |
+
norm_eps (`float`, *optional*, defaults to 1e-05):
|
| 45 |
+
The epsilon used by the RMS normalization layers.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
model_type = "lfm3_vl_vision"
|
| 49 |
+
base_config_key = "vision_config"
|
| 50 |
+
default_theta = 1_000_000.0
|
| 51 |
+
default_rope_type = "axial"
|
| 52 |
+
_attention_layer_types = ("full_attention",)
|
| 53 |
+
|
| 54 |
+
hidden_size: int = 1024
|
| 55 |
+
intermediate_size: int = 2560
|
| 56 |
+
num_hidden_layers: int = 14
|
| 57 |
+
num_attention_heads: int = 16
|
| 58 |
+
num_key_value_heads: int = 8
|
| 59 |
+
layer_types: list[str] | None = None
|
| 60 |
+
patch_size: int = 16
|
| 61 |
+
temporal_patch_size: int = 2
|
| 62 |
+
in_channels: int = 3
|
| 63 |
+
spatial_merge_size: int = 2
|
| 64 |
+
conv_kernel_size: int = 3
|
| 65 |
+
norm_eps: float = 1e-5
|
| 66 |
+
initializer_range: float = 0.02
|
| 67 |
+
rope_parameters: dict | None = None
|
| 68 |
+
|
| 69 |
+
def __post_init__(self, **kwargs):
|
| 70 |
+
if self.layer_types is None:
|
| 71 |
+
self.layer_types = ["conv" if i < 2 or i % 2 else "full_attention" for i in range(self.num_hidden_layers)]
|
| 72 |
+
super().__post_init__(**kwargs)
|
| 73 |
+
|
| 74 |
+
def validate_architecture(self):
|
| 75 |
+
"""Part of ``@strict``-powered validation."""
|
| 76 |
+
if len(self.layer_types) != self.num_hidden_layers:
|
| 77 |
+
raise ValueError(
|
| 78 |
+
f"`layer_types` has {len(self.layer_types)} entries but `num_hidden_layers` is {self.num_hidden_layers}."
|
| 79 |
+
)
|
| 80 |
+
allowed_layer_types = (*self._attention_layer_types, "conv")
|
| 81 |
+
invalid_layer_types = [layer_type for layer_type in self.layer_types if layer_type not in allowed_layer_types]
|
| 82 |
+
if invalid_layer_types:
|
| 83 |
+
raise ValueError(f"`layer_types` entries must be one of {allowed_layer_types}; got {invalid_layer_types}.")
|
| 84 |
+
if self.hidden_size % self.num_attention_heads:
|
| 85 |
+
raise ValueError("`hidden_size` must be a multiple of `num_attention_heads`.")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# The first checkpoints spell every text and `vision_*` field at the top level of the config
|
| 89 |
+
_LEGACY_TEXT_KEYS = (
|
| 90 |
+
"vocab_size",
|
| 91 |
+
"hidden_size",
|
| 92 |
+
"intermediate_size",
|
| 93 |
+
"num_hidden_layers",
|
| 94 |
+
"num_attention_heads",
|
| 95 |
+
"num_key_value_heads",
|
| 96 |
+
"head_dim",
|
| 97 |
+
"layer_types",
|
| 98 |
+
"conv_bias",
|
| 99 |
+
"conv_L_cache",
|
| 100 |
+
"sliding_window",
|
| 101 |
+
"max_position_embeddings",
|
| 102 |
+
"use_sandwich_norm",
|
| 103 |
+
"use_cache",
|
| 104 |
+
)
|
| 105 |
+
_LEGACY_VISION_KEYS = {
|
| 106 |
+
"vision_hidden_size": "hidden_size",
|
| 107 |
+
"vision_intermediate_size": "intermediate_size",
|
| 108 |
+
"vision_num_hidden_layers": "num_hidden_layers",
|
| 109 |
+
"vision_num_attention_heads": "num_attention_heads",
|
| 110 |
+
"vision_num_key_value_heads": "num_key_value_heads",
|
| 111 |
+
"vision_layer_types": "layer_types",
|
| 112 |
+
"vision_patch_size": "patch_size",
|
| 113 |
+
"vision_temporal_patch_size": "temporal_patch_size",
|
| 114 |
+
"vision_num_channels": "in_channels",
|
| 115 |
+
"vision_spatial_merge_size": "spatial_merge_size",
|
| 116 |
+
}
|
| 117 |
+
_LEGACY_DROPPED_KEYS = ("vision_patch_dim", "vision_max_position_embeddings")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@auto_docstring(checkpoint="LiquidAI/LFM3-VL-1.9B")
|
| 121 |
+
@strict
|
| 122 |
+
class Lfm3VlConfig(PreTrainedConfig):
|
| 123 |
+
r"""
|
| 124 |
+
video_token_id (`int`, *optional*, defaults to 125029):
|
| 125 |
+
The id of the placeholder token for video patches.
|
| 126 |
+
vision_start_token_id (`int`, *optional*, defaults to 125009):
|
| 127 |
+
The id of the token opening an image or a video frame.
|
| 128 |
+
vision_end_token_id (`int`, *optional*, defaults to 125010):
|
| 129 |
+
The id of the token closing an image or a video frame.
|
| 130 |
+
projector_layer_norm_eps (`float`, *optional*, defaults to 1e-06):
|
| 131 |
+
The epsilon of the layer normalization applied to the vision features before projection.
|
| 132 |
+
downsample_factor (`int`, *optional*, defaults to 2):
|
| 133 |
+
The side of the square block of vision tokens merged by the projector, equal to the vision encoder's
|
| 134 |
+
`spatial_merge_size`.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
model_type = "lfm3_vl"
|
| 138 |
+
sub_configs = {"text_config": AutoConfig, "vision_config": Lfm3VlVisionConfig}
|
| 139 |
+
|
| 140 |
+
text_config: dict | PreTrainedConfig | None = None
|
| 141 |
+
vision_config: dict | PreTrainedConfig | None = None
|
| 142 |
+
image_token_id: int = 125_030
|
| 143 |
+
video_token_id: int = 125_029
|
| 144 |
+
vision_start_token_id: int = 125_009
|
| 145 |
+
vision_end_token_id: int = 125_010
|
| 146 |
+
projector_hidden_act: str = "gelu"
|
| 147 |
+
projector_hidden_size: int = 4096
|
| 148 |
+
projector_bias: bool = True
|
| 149 |
+
projector_layer_norm_eps: float = 1e-6
|
| 150 |
+
downsample_factor: int = 2
|
| 151 |
+
tie_word_embeddings: bool = True
|
| 152 |
+
|
| 153 |
+
def __post_init__(self, **kwargs):
|
| 154 |
+
if "vision_hidden_size" in kwargs:
|
| 155 |
+
kwargs = self._convert_legacy_flat_config(kwargs)
|
| 156 |
+
|
| 157 |
+
if isinstance(self.vision_config, dict):
|
| 158 |
+
self.vision_config = self.sub_configs["vision_config"](**self.vision_config)
|
| 159 |
+
elif self.vision_config is None:
|
| 160 |
+
self.vision_config = self.sub_configs["vision_config"]()
|
| 161 |
+
|
| 162 |
+
if isinstance(self.text_config, dict):
|
| 163 |
+
self.text_config = {"model_type": "lfm3", **self.text_config}
|
| 164 |
+
self.text_config = _config_class(self.text_config["model_type"])(**self.text_config)
|
| 165 |
+
elif self.text_config is None:
|
| 166 |
+
self.text_config = _config_class("lfm3")()
|
| 167 |
+
self.validate_vision_fields()
|
| 168 |
+
|
| 169 |
+
super().__post_init__(**kwargs)
|
| 170 |
+
|
| 171 |
+
def validate_vision_fields(self):
|
| 172 |
+
"""Part of `@strict`-powered validation, so `validate()` and `save_pretrained` refuse later edits too."""
|
| 173 |
+
if self.downsample_factor != self.vision_config.spatial_merge_size:
|
| 174 |
+
raise ValueError("downsample_factor must match vision_config.spatial_merge_size")
|
| 175 |
+
# an `inputs_embeds` forward embeds both placeholder ids to find them
|
| 176 |
+
vocab_size = self.text_config.vocab_size
|
| 177 |
+
for name in ("image_token_id", "video_token_id"):
|
| 178 |
+
if not 0 <= getattr(self, name) < vocab_size:
|
| 179 |
+
raise ValueError(f"{name}={getattr(self, name)} must lie in [0, text_config.vocab_size={vocab_size})")
|
| 180 |
+
|
| 181 |
+
def _convert_legacy_flat_config(self, kwargs: dict) -> dict:
|
| 182 |
+
text_config = {key: kwargs.pop(key) for key in _LEGACY_TEXT_KEYS if key in kwargs}
|
| 183 |
+
text_config["norm_eps"] = kwargs.pop("rms_norm_eps", 1e-5)
|
| 184 |
+
text_config["rope_parameters"] = {
|
| 185 |
+
"full_attention": {
|
| 186 |
+
"rope_type": "default",
|
| 187 |
+
"rope_theta": kwargs.pop("full_attention_rope_theta", 1_000_000.0),
|
| 188 |
+
"partial_rotary_factor": kwargs.pop("partial_rotary_factor", 0.5),
|
| 189 |
+
},
|
| 190 |
+
"sliding_attention": {
|
| 191 |
+
"rope_type": "default",
|
| 192 |
+
"rope_theta": kwargs.pop("sliding_attention_rope_theta", 10_000.0),
|
| 193 |
+
},
|
| 194 |
+
}
|
| 195 |
+
for key in ("pad_token_id", "bos_token_id", "eos_token_id"):
|
| 196 |
+
if kwargs.get(key) is not None:
|
| 197 |
+
text_config[key] = kwargs[key]
|
| 198 |
+
text_config["tie_word_embeddings"] = self.tie_word_embeddings
|
| 199 |
+
|
| 200 |
+
vision_config = {new: kwargs.pop(old) for old, new in _LEGACY_VISION_KEYS.items() if old in kwargs}
|
| 201 |
+
vision_config["norm_eps"] = text_config["norm_eps"]
|
| 202 |
+
vision_config["rope_parameters"] = {"rope_type": "axial", "rope_theta": kwargs.pop("vision_rope_theta", 1e6)}
|
| 203 |
+
for key in _LEGACY_DROPPED_KEYS:
|
| 204 |
+
kwargs.pop(key, None)
|
| 205 |
+
|
| 206 |
+
self.text_config = text_config
|
| 207 |
+
self.vision_config = vision_config
|
| 208 |
+
return kwargs
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
__all__ = ["Lfm3VlConfig", "Lfm3VlVisionConfig"]
|
configuration_liquid_codec.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
|
| 17 |
+
from huggingface_hub.dataclasses import strict
|
| 18 |
+
|
| 19 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 20 |
+
from transformers.models.auto import CONFIG_MAPPING
|
| 21 |
+
from transformers.utils import auto_docstring
|
| 22 |
+
|
| 23 |
+
from .configuration_lfm3 import Lfm3Config
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _config_class(model_type):
|
| 27 |
+
# the LFM3 configs ship with the checkpoint instead of the transformers registry
|
| 28 |
+
remote = {"lfm3": Lfm3Config}
|
| 29 |
+
return remote.get(model_type) or CONFIG_MAPPING[model_type]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@auto_docstring(checkpoint="LiquidAI/Liquid-Codec")
|
| 33 |
+
@strict
|
| 34 |
+
class LiquidCodecConfig(PreTrainedConfig):
|
| 35 |
+
r"""
|
| 36 |
+
encoder_dim (`int`, *optional*, defaults to 512):
|
| 37 |
+
The width of the encoder and decoder LFM3 models.
|
| 38 |
+
encoder_conv_dim (`int`, *optional*, defaults to 256):
|
| 39 |
+
The number of channels of the STFT front end.
|
| 40 |
+
stft_n_fft (`int`, *optional*, defaults to 1280):
|
| 41 |
+
The FFT size of the analysis and synthesis transforms.
|
| 42 |
+
stft_hop_length (`int`, *optional*, defaults to 320):
|
| 43 |
+
The hop of the analysis and synthesis transforms.
|
| 44 |
+
stft_win_length (`int`, *optional*, defaults to 1280):
|
| 45 |
+
The window length of the analysis and synthesis transforms.
|
| 46 |
+
stft_frame_stack (`int`, *optional*, defaults to 6):
|
| 47 |
+
The number of STFT frames per codec frame.
|
| 48 |
+
quantizer_dim (`int`, *optional*, defaults to 256):
|
| 49 |
+
The width of the latent handed to the quantizer.
|
| 50 |
+
fsq_levels (`list[int]`, *optional*, defaults to `[8, 7, 6, 6]`):
|
| 51 |
+
The number of levels of each finite-scalar-quantization dimension; their product is the codebook size.
|
| 52 |
+
n_q (`int`, *optional*, defaults to 12):
|
| 53 |
+
The number of codebooks.
|
| 54 |
+
decoder_window (`int`, *optional*, defaults to 30):
|
| 55 |
+
The sliding attention window of the decoder LFM3 model, in 75 Hz frames.
|
| 56 |
+
sampling_rate (`int`, *optional*, defaults to 24000):
|
| 57 |
+
The sampling rate of the audio.
|
| 58 |
+
initializer_range (`float`, *optional*, defaults to 0.02):
|
| 59 |
+
The standard deviation of the truncated normal initializer of the weights.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
model_type = "liquid_codec"
|
| 63 |
+
|
| 64 |
+
encoder_dim: int = 512
|
| 65 |
+
encoder_conv_dim: int = 256
|
| 66 |
+
stft_n_fft: int = 1280
|
| 67 |
+
stft_hop_length: int = 320
|
| 68 |
+
stft_win_length: int = 1280
|
| 69 |
+
stft_frame_stack: int = 6
|
| 70 |
+
quantizer_dim: int = 256
|
| 71 |
+
fsq_levels: list[int] | None = None
|
| 72 |
+
n_q: int = 12
|
| 73 |
+
decoder_window: int = 30
|
| 74 |
+
sampling_rate: int = 24_000
|
| 75 |
+
initializer_range: float = 0.02
|
| 76 |
+
|
| 77 |
+
def __post_init__(self, **kwargs):
|
| 78 |
+
# the released configs nest the fields under `model` and dump the two LFM3 sub-configs
|
| 79 |
+
dropped = ("encoder_lfm_config", "decoder_lfm_config", "n_output_samples")
|
| 80 |
+
for key, value in kwargs.pop("model", {}).items():
|
| 81 |
+
if key in dropped:
|
| 82 |
+
continue
|
| 83 |
+
if key not in self.__dataclass_fields__:
|
| 84 |
+
raise ValueError(f"Unknown Liquid codec setting `{key}`")
|
| 85 |
+
setattr(self, key, value)
|
| 86 |
+
for key in dropped:
|
| 87 |
+
kwargs.pop(key, None)
|
| 88 |
+
if self.fsq_levels is None:
|
| 89 |
+
self.fsq_levels = [8, 7, 6, 6]
|
| 90 |
+
self.fsq_levels = list(self.fsq_levels)
|
| 91 |
+
super().__post_init__(**kwargs)
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def codebook_size(self) -> int:
|
| 95 |
+
return math.prod(self.fsq_levels)
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def frame_size(self) -> int:
|
| 99 |
+
return self.stft_hop_length * self.stft_frame_stack
|
| 100 |
+
|
| 101 |
+
def lfm_config(self, sliding_window: int | None = None) -> PreTrainedConfig:
|
| 102 |
+
attention_type = "full_attention" if sliding_window is None else "sliding_attention"
|
| 103 |
+
return _config_class("lfm3")(
|
| 104 |
+
# the backbones are not sub-configs, so the requested attention implementation is handed over here
|
| 105 |
+
attn_implementation=self._attn_implementation,
|
| 106 |
+
vocab_size=1,
|
| 107 |
+
pad_token_id=None,
|
| 108 |
+
bos_token_id=None,
|
| 109 |
+
eos_token_id=None,
|
| 110 |
+
hidden_size=self.encoder_dim,
|
| 111 |
+
intermediate_size=2304,
|
| 112 |
+
num_hidden_layers=8,
|
| 113 |
+
num_attention_heads=8,
|
| 114 |
+
num_key_value_heads=2,
|
| 115 |
+
head_dim=64,
|
| 116 |
+
max_position_embeddings=131_072,
|
| 117 |
+
norm_eps=1e-5,
|
| 118 |
+
conv_L_cache=7,
|
| 119 |
+
conv_bias=False,
|
| 120 |
+
use_sandwich_norm=True,
|
| 121 |
+
layer_types=[attention_type if i in (2, 4, 6) else "conv" for i in range(8)],
|
| 122 |
+
sliding_window=sliding_window,
|
| 123 |
+
rope_parameters={
|
| 124 |
+
attention_type: {"rope_type": "default", "rope_theta": 1_000_000.0, "partial_rotary_factor": 1.0}
|
| 125 |
+
},
|
| 126 |
+
use_cache=False,
|
| 127 |
+
tie_word_embeddings=False,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
__all__ = ["LiquidCodecConfig"]
|
feature_extraction_lfm3_omni.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
from transformers.audio_utils import mel_filter_bank
|
| 18 |
+
from transformers.feature_extraction_sequence_utils import SequenceFeatureExtractor
|
| 19 |
+
from transformers.feature_extraction_utils import BatchFeature
|
| 20 |
+
from transformers.utils import TensorType, is_torch_available, logging
|
| 21 |
+
|
| 22 |
+
from .modeling_lfm3_omni import Lfm3OmniFeatureStreamingCache
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
if is_torch_available():
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
logger = logging.get_logger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class Lfm3OmniFeatureExtractor(SequenceFeatureExtractor):
|
| 33 |
+
"""The LFM3-Audio log-mel front end; canonical checkpoints bundle its configuration files."""
|
| 34 |
+
|
| 35 |
+
model_input_names = ["input_features", "feature_attention_mask"]
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
feature_size: int = 128,
|
| 40 |
+
sampling_rate: int = 16000,
|
| 41 |
+
n_fft: int = 400,
|
| 42 |
+
hop_length: int = 160,
|
| 43 |
+
win_length: int = 400,
|
| 44 |
+
log_floor: float = -12.0,
|
| 45 |
+
normalization_offset: float = 4.0,
|
| 46 |
+
normalization_divisor: float = 4.0,
|
| 47 |
+
encoder_stride: int = 8,
|
| 48 |
+
padding_value: float = 0.0,
|
| 49 |
+
**kwargs,
|
| 50 |
+
):
|
| 51 |
+
super().__init__(feature_size=feature_size, sampling_rate=sampling_rate, padding_value=padding_value, **kwargs)
|
| 52 |
+
self.n_fft = n_fft
|
| 53 |
+
self.hop_length = hop_length
|
| 54 |
+
self.win_length = win_length
|
| 55 |
+
self.log_floor = log_floor
|
| 56 |
+
self.normalization_offset = normalization_offset
|
| 57 |
+
self.normalization_divisor = normalization_divisor
|
| 58 |
+
self.encoder_stride = encoder_stride
|
| 59 |
+
self.mel_filters = mel_filter_bank(
|
| 60 |
+
num_frequency_bins=n_fft // 2 + 1,
|
| 61 |
+
num_mel_filters=feature_size,
|
| 62 |
+
min_frequency=0.0,
|
| 63 |
+
max_frequency=sampling_rate / 2,
|
| 64 |
+
sampling_rate=sampling_rate,
|
| 65 |
+
norm="slaney",
|
| 66 |
+
mel_scale="slaney",
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
def num_frames(self, num_samples: int) -> int:
|
| 70 |
+
padded = self.pad_length(num_samples)
|
| 71 |
+
return 1 + (padded - self.n_fft) // self.hop_length
|
| 72 |
+
|
| 73 |
+
def pad_length(self, num_samples: int) -> int:
|
| 74 |
+
if num_samples < self.n_fft:
|
| 75 |
+
return self.n_fft
|
| 76 |
+
return num_samples + (-(num_samples - self.n_fft) % self.hop_length)
|
| 77 |
+
|
| 78 |
+
def num_encoder_tokens(self, num_frames: int) -> int:
|
| 79 |
+
return -(num_frames // -self.encoder_stride)
|
| 80 |
+
|
| 81 |
+
def _log_mel(self, waveform: torch.Tensor) -> torch.Tensor:
|
| 82 |
+
window = torch.hann_window(self.win_length, device=waveform.device)
|
| 83 |
+
spectrogram = torch.stft(
|
| 84 |
+
waveform,
|
| 85 |
+
n_fft=self.n_fft,
|
| 86 |
+
hop_length=self.hop_length,
|
| 87 |
+
win_length=self.win_length,
|
| 88 |
+
window=window,
|
| 89 |
+
center=False,
|
| 90 |
+
return_complex=True,
|
| 91 |
+
)
|
| 92 |
+
power = spectrogram.abs().square()
|
| 93 |
+
mel_filters = torch.from_numpy(self.mel_filters).to(device=waveform.device, dtype=power.dtype)
|
| 94 |
+
log_mel = (mel_filters.T @ power).log10().clamp(min=self.log_floor)
|
| 95 |
+
return (log_mel + self.normalization_offset) / self.normalization_divisor
|
| 96 |
+
|
| 97 |
+
@torch.no_grad()
|
| 98 |
+
def stream(
|
| 99 |
+
self,
|
| 100 |
+
raw_speech: np.ndarray | torch.Tensor | list[float],
|
| 101 |
+
cache: Lfm3OmniFeatureStreamingCache | None = None,
|
| 102 |
+
is_final: bool = False,
|
| 103 |
+
sampling_rate: int | None = None,
|
| 104 |
+
) -> tuple[BatchFeature, Lfm3OmniFeatureStreamingCache]:
|
| 105 |
+
"""Extract completed windows from a mono chunk, retaining overlap. Finalization pads the last window."""
|
| 106 |
+
if sampling_rate is not None and sampling_rate != self.sampling_rate:
|
| 107 |
+
raise ValueError(f"Expected {self.sampling_rate} Hz, got {sampling_rate} Hz")
|
| 108 |
+
if cache is None:
|
| 109 |
+
cache = Lfm3OmniFeatureStreamingCache()
|
| 110 |
+
if cache.finished:
|
| 111 |
+
raise ValueError("This stream is finalized; start with a new cache")
|
| 112 |
+
chunk = torch.as_tensor(raw_speech, dtype=torch.float32)
|
| 113 |
+
if chunk.ndim != 1:
|
| 114 |
+
raise ValueError("stream expects one mono waveform")
|
| 115 |
+
samples = chunk if cache.samples is None else torch.cat((cache.samples, chunk))
|
| 116 |
+
num_samples = cache.num_samples + chunk.numel()
|
| 117 |
+
if is_final:
|
| 118 |
+
count = self.num_frames(num_samples) - cache.num_frames
|
| 119 |
+
if count:
|
| 120 |
+
size = (count - 1) * self.hop_length + self.n_fft
|
| 121 |
+
samples = torch.nn.functional.pad(samples, (0, size - samples.numel()))
|
| 122 |
+
else:
|
| 123 |
+
count = max(0, 1 + (samples.numel() - self.n_fft) // self.hop_length)
|
| 124 |
+
features = self._log_mel(samples) if count else samples.new_empty((self.feature_size, 0))
|
| 125 |
+
# the cache changes only once the chunk's features exist, so a failed call can be retried
|
| 126 |
+
cache.samples = samples[count * self.hop_length :].clone() if not is_final else None
|
| 127 |
+
cache.num_samples = num_samples
|
| 128 |
+
cache.num_frames += count
|
| 129 |
+
cache.finished = is_final
|
| 130 |
+
return BatchFeature(
|
| 131 |
+
{
|
| 132 |
+
"input_features": features[None],
|
| 133 |
+
"feature_attention_mask": torch.ones((1, count), device=chunk.device, dtype=torch.long),
|
| 134 |
+
}
|
| 135 |
+
), cache
|
| 136 |
+
|
| 137 |
+
def __call__(
|
| 138 |
+
self,
|
| 139 |
+
raw_speech: np.ndarray | torch.Tensor | list[np.ndarray] | list[torch.Tensor] | list[float],
|
| 140 |
+
sampling_rate: int | None = None,
|
| 141 |
+
return_tensors: str | TensorType | None = None,
|
| 142 |
+
**kwargs,
|
| 143 |
+
) -> BatchFeature:
|
| 144 |
+
r"""
|
| 145 |
+
raw_speech (`np.ndarray`, `torch.Tensor`, `list[np.ndarray]`, `list[torch.Tensor]`):
|
| 146 |
+
One mono clip or a list of mono clips, as float samples at `sampling_rate`.
|
| 147 |
+
sampling_rate (`int`, *optional*):
|
| 148 |
+
The sampling rate of `raw_speech`, checked against the extractor's.
|
| 149 |
+
return_tensors (`str` or [`~utils.TensorType`], *optional*):
|
| 150 |
+
If set, will return tensors instead of numpy arrays.
|
| 151 |
+
"""
|
| 152 |
+
if kwargs:
|
| 153 |
+
raise TypeError(
|
| 154 |
+
f"{self.__class__.__name__} does not implement {sorted(kwargs)}: clips are always right-padded to "
|
| 155 |
+
"whole frames and `feature_attention_mask` is always returned"
|
| 156 |
+
)
|
| 157 |
+
if sampling_rate is not None and sampling_rate != self.sampling_rate:
|
| 158 |
+
raise ValueError(
|
| 159 |
+
f"{self.__class__.__name__} expects audio sampled at {self.sampling_rate} Hz, got {sampling_rate} Hz"
|
| 160 |
+
)
|
| 161 |
+
if sampling_rate is None:
|
| 162 |
+
logger.warning_once(
|
| 163 |
+
f"It is strongly recommended to pass the `sampling_rate` argument to `{self.__class__.__name__}()` "
|
| 164 |
+
"to avoid silent errors."
|
| 165 |
+
)
|
| 166 |
+
if isinstance(raw_speech, (list, tuple)) and len(raw_speech) == 0:
|
| 167 |
+
raise ValueError(f"{self.__class__.__name__} needs at least one clip")
|
| 168 |
+
is_batched = isinstance(raw_speech, (list, tuple)) and hasattr(raw_speech[0], "__len__")
|
| 169 |
+
is_batched = is_batched or (isinstance(raw_speech, (np.ndarray, torch.Tensor)) and raw_speech.ndim == 2)
|
| 170 |
+
clips = list(raw_speech) if is_batched else [raw_speech]
|
| 171 |
+
clips = [
|
| 172 |
+
clip.detach().cpu().float()
|
| 173 |
+
if isinstance(clip, torch.Tensor)
|
| 174 |
+
else torch.as_tensor(np.asarray(clip, dtype=np.float32))
|
| 175 |
+
for clip in clips
|
| 176 |
+
]
|
| 177 |
+
for clip in clips:
|
| 178 |
+
if clip.ndim != 1:
|
| 179 |
+
raise ValueError(
|
| 180 |
+
f"{self.__class__.__name__} expects mono clips, got a clip of shape {tuple(clip.shape)}"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
features = []
|
| 184 |
+
for clip in clips:
|
| 185 |
+
clip = torch.nn.functional.pad(clip, (0, self.pad_length(clip.shape[0]) - clip.shape[0]))
|
| 186 |
+
features.append(self._log_mel(clip))
|
| 187 |
+
lengths = [feature.shape[-1] for feature in features]
|
| 188 |
+
max_length = max(lengths)
|
| 189 |
+
input_features = torch.full((len(features), self.feature_size, max_length), float(self.padding_value))
|
| 190 |
+
feature_attention_mask = torch.zeros((len(features), max_length), dtype=torch.long)
|
| 191 |
+
for index, feature in enumerate(features):
|
| 192 |
+
input_features[index, :, : feature.shape[-1]] = feature
|
| 193 |
+
feature_attention_mask[index, : feature.shape[-1]] = 1
|
| 194 |
+
|
| 195 |
+
data = {
|
| 196 |
+
"input_features": input_features.numpy(),
|
| 197 |
+
"feature_attention_mask": feature_attention_mask.numpy(),
|
| 198 |
+
}
|
| 199 |
+
return BatchFeature(data=data, tensor_type=return_tensors)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
__all__ = ["Lfm3OmniFeatureExtractor"]
|
generation_config.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"bos_token_id": 124894,
|
| 4 |
+
"eos_token_id": 124900,
|
| 5 |
+
"output_attentions": false,
|
| 6 |
+
"output_hidden_states": false,
|
| 7 |
+
"pad_token_id": 124893,
|
| 8 |
+
"transformers_version": "5.20.0",
|
| 9 |
+
"use_cache": true
|
| 10 |
+
}
|
image_processing_lfm3_vl.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import math
|
| 15 |
+
import warnings
|
| 16 |
+
from collections.abc import Iterable
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
from torchvision.transforms.v2 import functional as tvF
|
| 20 |
+
|
| 21 |
+
from transformers.image_processing_backends import TorchvisionBackend
|
| 22 |
+
from transformers.image_processing_utils import BatchFeature
|
| 23 |
+
from transformers.image_transforms import group_images_by_shape, reorder_images
|
| 24 |
+
from transformers.image_utils import ImageInput, PILImageResampling, SizeDict
|
| 25 |
+
from transformers.processing_utils import ImagesKwargs, Unpack
|
| 26 |
+
from transformers.utils import TensorType, auto_docstring
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Lfm3VlImageProcessorKwargs(ImagesKwargs, total=False):
|
| 30 |
+
r"""
|
| 31 |
+
min_pixels (`int`, *optional*, defaults to `56 * 56`):
|
| 32 |
+
The min pixels of the image to resize the image.
|
| 33 |
+
max_pixels (`int`, *optional*, defaults to `28 * 28 * 1280`):
|
| 34 |
+
The max pixels of the image to resize the image.
|
| 35 |
+
patch_size (`int`, *optional*, defaults to 14):
|
| 36 |
+
The spatial patch size of the vision encoder.
|
| 37 |
+
temporal_patch_size (`int`, *optional*, defaults to 2):
|
| 38 |
+
The temporal patch size of the vision encoder.
|
| 39 |
+
merge_size (`int`, *optional*, defaults to 2):
|
| 40 |
+
The merge size of the vision encoder to llm encoder.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
min_pixels: int
|
| 44 |
+
max_pixels: int
|
| 45 |
+
patch_size: int
|
| 46 |
+
temporal_patch_size: int
|
| 47 |
+
merge_size: int
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def smart_resize(
|
| 51 |
+
height: int, width: int, factor: int = 28, min_pixels: int = 56 * 56, max_pixels: int = 14 * 14 * 4 * 1280
|
| 52 |
+
):
|
| 53 |
+
"""Rescales the image so that the following conditions are met:
|
| 54 |
+
|
| 55 |
+
1. Both dimensions (height and width) are divisible by 'factor'.
|
| 56 |
+
|
| 57 |
+
2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
|
| 58 |
+
|
| 59 |
+
3. The aspect ratio of the image is maintained as closely as possible.
|
| 60 |
+
|
| 61 |
+
"""
|
| 62 |
+
if max(height, width) / min(height, width) > 200:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
f"absolute aspect ratio must be smaller than 200, got {max(height, width) / min(height, width)}"
|
| 65 |
+
)
|
| 66 |
+
h_bar = round(height / factor) * factor
|
| 67 |
+
w_bar = round(width / factor) * factor
|
| 68 |
+
if h_bar * w_bar > max_pixels:
|
| 69 |
+
beta = math.sqrt((height * width) / max_pixels)
|
| 70 |
+
h_bar = max(factor, math.floor(height / beta / factor) * factor)
|
| 71 |
+
w_bar = max(factor, math.floor(width / beta / factor) * factor)
|
| 72 |
+
elif h_bar * w_bar < min_pixels:
|
| 73 |
+
beta = math.sqrt(min_pixels / (height * width))
|
| 74 |
+
h_bar = math.ceil(height * beta / factor) * factor
|
| 75 |
+
w_bar = math.ceil(width * beta / factor) * factor
|
| 76 |
+
return h_bar, w_bar
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@auto_docstring
|
| 80 |
+
class Lfm3VlImageProcessor(TorchvisionBackend):
|
| 81 |
+
do_resize = True
|
| 82 |
+
resample = PILImageResampling.BICUBIC
|
| 83 |
+
size = {"shortest_edge": 65_536, "longest_edge": 16_777_216}
|
| 84 |
+
default_to_square = False
|
| 85 |
+
do_rescale = True
|
| 86 |
+
do_normalize = True
|
| 87 |
+
image_mean = [0.5, 0.5, 0.5]
|
| 88 |
+
image_std = [0.5, 0.5, 0.5]
|
| 89 |
+
do_convert_rgb = True
|
| 90 |
+
patch_size = 16
|
| 91 |
+
temporal_patch_size = 2
|
| 92 |
+
merge_size = 2
|
| 93 |
+
valid_kwargs = Lfm3VlImageProcessorKwargs
|
| 94 |
+
model_input_names = ["pixel_values", "image_grid_thw"]
|
| 95 |
+
|
| 96 |
+
def __init__(self, **kwargs: Unpack[Lfm3VlImageProcessorKwargs]):
|
| 97 |
+
# backward compatibility: override size with min_pixels and max_pixels if they are provided
|
| 98 |
+
size = kwargs.pop("size", None)
|
| 99 |
+
size = dict(self.size) if size is None else size
|
| 100 |
+
if (min_pixels := kwargs.pop("min_pixels", None)) is not None:
|
| 101 |
+
size["shortest_edge"] = min_pixels
|
| 102 |
+
size.pop("min_pixels", None)
|
| 103 |
+
if (max_pixels := kwargs.pop("max_pixels", None)) is not None:
|
| 104 |
+
size["longest_edge"] = max_pixels
|
| 105 |
+
size.pop("max_pixels", None)
|
| 106 |
+
super().__init__(size=size, **kwargs)
|
| 107 |
+
|
| 108 |
+
def _standardize_kwargs(
|
| 109 |
+
self,
|
| 110 |
+
size: int | Iterable[int] | dict[str, int] | SizeDict | None = None,
|
| 111 |
+
min_pixels: int | None = None,
|
| 112 |
+
max_pixels: int | None = None,
|
| 113 |
+
**kwargs,
|
| 114 |
+
) -> dict:
|
| 115 |
+
if min_pixels is not None or max_pixels is not None:
|
| 116 |
+
warnings.warn(
|
| 117 |
+
"Passing `min_pixels` and `max_pixels` to a processor call is deprecated and will be removed in v5.23. "
|
| 118 |
+
"Pass in `size={'longest_edge': xxx, 'shortest_edge': xxx} to override the target size.`",
|
| 119 |
+
FutureWarning,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
size_dict = dict(size) if isinstance(size, (dict, SizeDict)) else {}
|
| 123 |
+
if min_pixels is not None:
|
| 124 |
+
size_dict["shortest_edge"] = min_pixels
|
| 125 |
+
if max_pixels is not None:
|
| 126 |
+
size_dict["longest_edge"] = max_pixels
|
| 127 |
+
size = SizeDict(**size_dict)
|
| 128 |
+
return super()._standardize_kwargs(size=size, **kwargs)
|
| 129 |
+
|
| 130 |
+
@auto_docstring
|
| 131 |
+
def preprocess(
|
| 132 |
+
self,
|
| 133 |
+
images: ImageInput,
|
| 134 |
+
**kwargs: Unpack[Lfm3VlImageProcessorKwargs],
|
| 135 |
+
) -> BatchFeature:
|
| 136 |
+
return super().preprocess(images, **kwargs)
|
| 137 |
+
|
| 138 |
+
def resize(
|
| 139 |
+
self,
|
| 140 |
+
images: "torch.Tensor",
|
| 141 |
+
size: SizeDict,
|
| 142 |
+
resample: "PILImageResampling | tvF.InterpolationMode | int | None",
|
| 143 |
+
factor: int,
|
| 144 |
+
**kwargs,
|
| 145 |
+
) -> "torch.Tensor":
|
| 146 |
+
"""Resize dynamically based on input image aspect ratio."""
|
| 147 |
+
if not size.shortest_edge or not size.longest_edge:
|
| 148 |
+
raise ValueError(f"`size` dict must contain 'shortest_edge' and 'longest_edge' keys but got {size}.")
|
| 149 |
+
|
| 150 |
+
height, width = images.shape[-2:]
|
| 151 |
+
resized_height, resized_width = smart_resize(
|
| 152 |
+
height,
|
| 153 |
+
width,
|
| 154 |
+
factor=factor,
|
| 155 |
+
min_pixels=size.shortest_edge,
|
| 156 |
+
max_pixels=size.longest_edge,
|
| 157 |
+
)
|
| 158 |
+
return super().resize(
|
| 159 |
+
image=images,
|
| 160 |
+
size=SizeDict(height=resized_height, width=resized_width),
|
| 161 |
+
resample=resample,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def patchify(
|
| 165 |
+
self,
|
| 166 |
+
images: "torch.Tensor",
|
| 167 |
+
patch_size: int,
|
| 168 |
+
merge_size: int,
|
| 169 |
+
temporal_patch_size: int,
|
| 170 |
+
) -> tuple["torch.Tensor", int, int]:
|
| 171 |
+
"""Patchifies each image into flat layout of shape (`seq_len`, `patch_dim`) so we can concat dynamically shaped pixels."""
|
| 172 |
+
batch_size, channel, resized_height, resized_width = images.shape
|
| 173 |
+
grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
|
| 174 |
+
patches = images.reshape(
|
| 175 |
+
batch_size,
|
| 176 |
+
channel,
|
| 177 |
+
grid_h // merge_size,
|
| 178 |
+
merge_size,
|
| 179 |
+
patch_size,
|
| 180 |
+
grid_w // merge_size,
|
| 181 |
+
merge_size,
|
| 182 |
+
patch_size,
|
| 183 |
+
)
|
| 184 |
+
# Reorder dimensions to group grid and patch information for subsequent flattening.
|
| 185 |
+
# [batch, grid_h/merge, grid_w/merge, merge, merge, channel, patch, patch]
|
| 186 |
+
patches = patches.permute(0, 2, 5, 3, 6, 1, 4, 7)
|
| 187 |
+
|
| 188 |
+
flatten_patches = (
|
| 189 |
+
patches.unsqueeze(6)
|
| 190 |
+
.expand(-1, -1, -1, -1, -1, -1, temporal_patch_size, -1, -1)
|
| 191 |
+
.reshape(
|
| 192 |
+
batch_size,
|
| 193 |
+
grid_h * grid_w,
|
| 194 |
+
channel * temporal_patch_size * patch_size * patch_size,
|
| 195 |
+
)
|
| 196 |
+
)
|
| 197 |
+
return flatten_patches, grid_h, grid_w
|
| 198 |
+
|
| 199 |
+
def _preprocess(
|
| 200 |
+
self,
|
| 201 |
+
images: list["torch.Tensor"],
|
| 202 |
+
do_resize: bool,
|
| 203 |
+
size: SizeDict,
|
| 204 |
+
resample: "PILImageResampling | tvF.InterpolationMode | int | None",
|
| 205 |
+
do_rescale: bool,
|
| 206 |
+
rescale_factor: float,
|
| 207 |
+
do_normalize: bool,
|
| 208 |
+
image_mean: float | list[float] | None,
|
| 209 |
+
image_std: float | list[float] | None,
|
| 210 |
+
patch_size: int,
|
| 211 |
+
temporal_patch_size: int,
|
| 212 |
+
merge_size: int,
|
| 213 |
+
disable_grouping: bool | None,
|
| 214 |
+
return_tensors: str | TensorType | None,
|
| 215 |
+
**kwargs,
|
| 216 |
+
) -> BatchFeature:
|
| 217 |
+
grouped_images, grouped_images_index = group_images_by_shape(images, disable_grouping=disable_grouping)
|
| 218 |
+
resized_images_grouped = {}
|
| 219 |
+
for shape, stacked_images in grouped_images.items():
|
| 220 |
+
if do_resize:
|
| 221 |
+
stacked_images = self.resize(
|
| 222 |
+
images=stacked_images,
|
| 223 |
+
size=size,
|
| 224 |
+
resample=resample,
|
| 225 |
+
factor=patch_size * merge_size,
|
| 226 |
+
)
|
| 227 |
+
resized_images_grouped[shape] = stacked_images
|
| 228 |
+
resized_images = reorder_images(resized_images_grouped, grouped_images_index)
|
| 229 |
+
|
| 230 |
+
grouped_images, grouped_images_index = group_images_by_shape(resized_images, disable_grouping=disable_grouping)
|
| 231 |
+
processed_images_grouped = {}
|
| 232 |
+
processed_grids = {}
|
| 233 |
+
for shape, stacked_images in grouped_images.items():
|
| 234 |
+
stacked_images = self.rescale_and_normalize(
|
| 235 |
+
stacked_images, do_rescale, rescale_factor, do_normalize, image_mean, image_std
|
| 236 |
+
)
|
| 237 |
+
patches, grid_h, grid_w = self.patchify(
|
| 238 |
+
stacked_images,
|
| 239 |
+
patch_size=patch_size,
|
| 240 |
+
merge_size=merge_size,
|
| 241 |
+
temporal_patch_size=temporal_patch_size,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
processed_images_grouped[shape] = patches
|
| 245 |
+
processed_grids[shape] = [[1, grid_h, grid_w]] * len(stacked_images)
|
| 246 |
+
|
| 247 |
+
processed_images = reorder_images(processed_images_grouped, grouped_images_index)
|
| 248 |
+
processed_grids_ordered = reorder_images(processed_grids, grouped_images_index)
|
| 249 |
+
pixel_values = processed_images[0] if len(processed_images) == 1 else torch.cat(processed_images, dim=0)
|
| 250 |
+
image_grid_thw = torch.tensor(processed_grids_ordered, dtype=torch.long)
|
| 251 |
+
|
| 252 |
+
return BatchFeature(
|
| 253 |
+
data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}, tensor_type=return_tensors
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
def get_number_of_image_patches(self, height: int, width: int, images_kwargs: dict | None = None) -> int:
|
| 257 |
+
"""
|
| 258 |
+
A utility that returns number of image patches for a given image size.
|
| 259 |
+
|
| 260 |
+
Note: Do not remove this method! It is used by vLLM to infer the number of patches and placeholders
|
| 261 |
+
without an image input.
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
height (`int`):
|
| 265 |
+
Height of the input image.
|
| 266 |
+
width (`int`):
|
| 267 |
+
Width of the input image.
|
| 268 |
+
images_kwargs (`dict`, *optional*)
|
| 269 |
+
Any kwargs to override defaults of the image processor.
|
| 270 |
+
Returns:
|
| 271 |
+
`int`: Number of image patches per image.
|
| 272 |
+
"""
|
| 273 |
+
# `size` is the supported override (`min_pixels` / `max_pixels` are deprecated): count with the budget the
|
| 274 |
+
# preprocessing applies
|
| 275 |
+
images_kwargs = dict(images_kwargs or {})
|
| 276 |
+
size = images_kwargs.pop("size", None) or self.size
|
| 277 |
+
images_kwargs.setdefault("min_pixels", size["shortest_edge"])
|
| 278 |
+
images_kwargs.setdefault("max_pixels", size["longest_edge"])
|
| 279 |
+
images_kwargs = images_kwargs or {}
|
| 280 |
+
min_pixels = images_kwargs["min_pixels"] if "min_pixels" in images_kwargs else self.size["shortest_edge"]
|
| 281 |
+
max_pixels = images_kwargs["max_pixels"] if "max_pixels" in images_kwargs else self.size["longest_edge"]
|
| 282 |
+
patch_size = images_kwargs.get("patch_size", self.patch_size)
|
| 283 |
+
merge_size = images_kwargs.get("merge_size", self.merge_size)
|
| 284 |
+
|
| 285 |
+
factor = patch_size * merge_size
|
| 286 |
+
resized_height, resized_width = smart_resize(
|
| 287 |
+
height, width, factor, min_pixels=min_pixels, max_pixels=max_pixels
|
| 288 |
+
)
|
| 289 |
+
grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
|
| 290 |
+
return grid_h * grid_w
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
__all__ = ["Lfm3VlImageProcessor"]
|
lfm3_conv.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Inference kernels for LFM3's width-7 depthwise convolution."""
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from transformers.utils import is_triton_available
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if is_triton_available():
|
| 22 |
+
import triton
|
| 23 |
+
import triton.language as tl
|
| 24 |
+
|
| 25 |
+
# the length and strides are runtime arguments, or every prompt length would compile its own kernel
|
| 26 |
+
@triton.jit(do_not_specialize=["T", "X0", "X1", "X2", "S0", "S1", "ST"])
|
| 27 |
+
def _prefill(
|
| 28 |
+
X,
|
| 29 |
+
W,
|
| 30 |
+
BIAS,
|
| 31 |
+
SEQ,
|
| 32 |
+
Y,
|
| 33 |
+
T,
|
| 34 |
+
D: tl.constexpr,
|
| 35 |
+
X0,
|
| 36 |
+
X1,
|
| 37 |
+
X2,
|
| 38 |
+
S0,
|
| 39 |
+
S1,
|
| 40 |
+
ST,
|
| 41 |
+
HAS_BIAS: tl.constexpr,
|
| 42 |
+
PACKED: tl.constexpr,
|
| 43 |
+
BLOCK: tl.constexpr,
|
| 44 |
+
):
|
| 45 |
+
# int64 offsets: batch * channels * length can exceed 2**31 elements
|
| 46 |
+
b, d = tl.program_id(0).to(tl.int64), tl.program_id(1).to(tl.int64)
|
| 47 |
+
t = tl.program_id(2).to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
|
| 48 |
+
acc = tl.full((BLOCK,), 0, tl.float32)
|
| 49 |
+
if PACKED:
|
| 50 |
+
doc = tl.load(SEQ + b * S0 + (t - T + ST) * S1, (t < T) & (t >= T - ST), other=-1)
|
| 51 |
+
for k in tl.static_range(7):
|
| 52 |
+
pos = t + k - 6
|
| 53 |
+
valid = (t < T) & (pos >= 0)
|
| 54 |
+
if PACKED:
|
| 55 |
+
prev = tl.load(SEQ + b * S0 + (pos - T + ST) * S1, valid & (pos >= T - ST), other=-1)
|
| 56 |
+
valid = valid & (doc == prev)
|
| 57 |
+
x = tl.load(X + b * X0 + d * X1 + pos * X2, valid, other=0).to(tl.float32)
|
| 58 |
+
w = tl.load(W + d * 7 + k).to(tl.float32)
|
| 59 |
+
acc += x * w
|
| 60 |
+
if HAS_BIAS:
|
| 61 |
+
acc += tl.load(BIAS + d).to(tl.float32)
|
| 62 |
+
tl.store(Y + (b * D + d) * T + t, acc, t < T)
|
| 63 |
+
|
| 64 |
+
@triton.jit
|
| 65 |
+
def _decode(
|
| 66 |
+
X,
|
| 67 |
+
W,
|
| 68 |
+
BIAS,
|
| 69 |
+
STATE,
|
| 70 |
+
Y,
|
| 71 |
+
D: tl.constexpr,
|
| 72 |
+
K: tl.constexpr,
|
| 73 |
+
X0: tl.constexpr,
|
| 74 |
+
X1: tl.constexpr,
|
| 75 |
+
S0: tl.constexpr,
|
| 76 |
+
S1: tl.constexpr,
|
| 77 |
+
S2: tl.constexpr,
|
| 78 |
+
HAS_BIAS: tl.constexpr,
|
| 79 |
+
BLOCK: tl.constexpr,
|
| 80 |
+
):
|
| 81 |
+
b = tl.program_id(0).to(tl.int64)
|
| 82 |
+
d = tl.program_id(1).to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
|
| 83 |
+
k = tl.arange(0, 8)
|
| 84 |
+
# Load the complete state before writing the shifted window.
|
| 85 |
+
old = tl.load(STATE + b * S0 + d[:, None] * S1 + k[None, :] * S2, (d[:, None] < D) & (k[None, :] < K), other=0)
|
| 86 |
+
x = tl.load(X + b * X0 + d * X1, d < D, other=0)
|
| 87 |
+
shifted = tl.gather(old, tl.broadcast_to(tl.minimum(k + 1, 7)[None, :], (BLOCK, 8)), 1)
|
| 88 |
+
shifted = tl.where(k[None, :] == K - 1, x[:, None], shifted)
|
| 89 |
+
# A cache may store either K=6 previous inputs or K=7 including the oldest discarded input.
|
| 90 |
+
positions = k + K - 6
|
| 91 |
+
values = tl.gather(old, tl.broadcast_to(tl.minimum(positions, 7)[None, :], (BLOCK, 8)), 1)
|
| 92 |
+
values = tl.where(k[None, :] == 6, x[:, None], values)
|
| 93 |
+
weights = tl.load(W + d[:, None] * 7 + k[None, :], (d[:, None] < D) & (k[None, :] < 7), other=0)
|
| 94 |
+
acc = tl.sum(values.to(tl.float32) * weights.to(tl.float32), 1)
|
| 95 |
+
if HAS_BIAS:
|
| 96 |
+
acc += tl.load(BIAS + d, d < D, other=0).to(tl.float32)
|
| 97 |
+
tl.store(STATE + b * S0 + d[:, None] * S1 + k[None, :] * S2, shifted, (d[:, None] < D) & (k[None, :] < K))
|
| 98 |
+
tl.store(Y + b * D + d, acc, d < D)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def can_use_lfm3_conv(hidden_states, weight):
|
| 102 |
+
return (
|
| 103 |
+
is_triton_available()
|
| 104 |
+
and hasattr(tl, "gather") # the decode kernel needs Triton >= 3.3
|
| 105 |
+
and hidden_states.is_cuda
|
| 106 |
+
and not torch.is_grad_enabled()
|
| 107 |
+
and hidden_states.dtype == weight.dtype
|
| 108 |
+
and hidden_states.dtype in (torch.float16, torch.bfloat16, torch.float32)
|
| 109 |
+
and weight.shape[-1] == 7
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def lfm3_causal_conv1d(hidden_states, weight, bias=None, seq_idx=None):
|
| 114 |
+
batch, channels, length = hidden_states.shape
|
| 115 |
+
# the kernels index every tensor by the batch and channel of `hidden_states` and cannot bound the others
|
| 116 |
+
if weight.shape != (channels, 7) or (bias is not None and bias.shape != (channels,)):
|
| 117 |
+
raise ValueError(
|
| 118 |
+
f"`weight` must have shape ({channels}, 7) and `bias` ({channels},), got {tuple(weight.shape)}"
|
| 119 |
+
)
|
| 120 |
+
if seq_idx is not None and (
|
| 121 |
+
seq_idx.ndim != 2 or seq_idx.shape[0] not in (1, batch) or seq_idx.dtype not in (torch.int32, torch.int64)
|
| 122 |
+
):
|
| 123 |
+
raise ValueError(
|
| 124 |
+
f"`seq_idx` must be an int32 or int64 tensor of shape (1, length) or ({batch}, length), got "
|
| 125 |
+
f"{seq_idx.dtype} {tuple(seq_idx.shape)}"
|
| 126 |
+
)
|
| 127 |
+
if any(t is not None and t.device != hidden_states.device for t in (weight, bias, seq_idx)):
|
| 128 |
+
raise ValueError(
|
| 129 |
+
f"`weight`, `bias` and `seq_idx` must be on the device of `hidden_states` ({hidden_states.device})"
|
| 130 |
+
)
|
| 131 |
+
output = torch.empty_like(hidden_states, memory_format=torch.contiguous_format)
|
| 132 |
+
# Triton launches on the current device; the model may live on another one (`device_map`, `.to("cuda:1")`).
|
| 133 |
+
with torch.cuda.device(hidden_states.device):
|
| 134 |
+
_prefill[(batch, channels, triton.cdiv(length, 128))](
|
| 135 |
+
hidden_states,
|
| 136 |
+
# the kernels index `bias` and `weight` densely
|
| 137 |
+
weight.contiguous(),
|
| 138 |
+
bias.contiguous() if bias is not None else None,
|
| 139 |
+
seq_idx,
|
| 140 |
+
output,
|
| 141 |
+
length,
|
| 142 |
+
channels,
|
| 143 |
+
*hidden_states.stride(),
|
| 144 |
+
*(
|
| 145 |
+
(0 if seq_idx.shape[0] == 1 else seq_idx.stride(0), seq_idx.stride(1))
|
| 146 |
+
if seq_idx is not None
|
| 147 |
+
else (0, 0)
|
| 148 |
+
),
|
| 149 |
+
seq_idx.shape[-1] if seq_idx is not None else 0,
|
| 150 |
+
bias is not None,
|
| 151 |
+
seq_idx is not None,
|
| 152 |
+
128,
|
| 153 |
+
enable_fp_fusion=False,
|
| 154 |
+
)
|
| 155 |
+
return output
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def lfm3_causal_conv1d_update(hidden_states, conv_state, weight, bias=None):
|
| 159 |
+
batch, channels, length = hidden_states.shape
|
| 160 |
+
# the kernel addresses one position per row and bounds nothing by the other tensors' shapes
|
| 161 |
+
if length != 1:
|
| 162 |
+
raise ValueError(f"`hidden_states` must hold one position per row, got {tuple(hidden_states.shape)}")
|
| 163 |
+
# the 8-wide tile holds a 6- or 7-wide state (see `positions` in `_decode`)
|
| 164 |
+
if (
|
| 165 |
+
conv_state.ndim != 3
|
| 166 |
+
or conv_state.shape[:2] != (batch, channels)
|
| 167 |
+
or conv_state.shape[-1] not in (6, 7)
|
| 168 |
+
or weight.shape != (channels, 7)
|
| 169 |
+
):
|
| 170 |
+
raise ValueError(
|
| 171 |
+
f"`conv_state` must have shape ({batch}, {channels}, 6 or 7) and `weight` ({channels}, 7), got "
|
| 172 |
+
f"{tuple(conv_state.shape)} and {tuple(weight.shape)}"
|
| 173 |
+
)
|
| 174 |
+
if bias is not None and bias.shape != (channels,):
|
| 175 |
+
raise ValueError(f"`bias` must have shape ({channels},), got {tuple(bias.shape)}")
|
| 176 |
+
# the kernel writes the state in place: rows or channels sharing memory (an `expand`ed cache) would race
|
| 177 |
+
extent = 0
|
| 178 |
+
for stride, size in sorted(zip(conv_state.stride(), conv_state.shape)):
|
| 179 |
+
if size > 1 and stride <= extent:
|
| 180 |
+
raise ValueError(f"`conv_state` must not overlap itself, got strides {conv_state.stride()}")
|
| 181 |
+
extent += stride * (size - 1)
|
| 182 |
+
if any(t is not None and t.device != hidden_states.device for t in (conv_state, weight, bias)):
|
| 183 |
+
raise ValueError(
|
| 184 |
+
f"`conv_state`, `weight` and `bias` must be on the device of `hidden_states` ({hidden_states.device})"
|
| 185 |
+
)
|
| 186 |
+
output = torch.empty_like(hidden_states, memory_format=torch.contiguous_format)
|
| 187 |
+
with torch.cuda.device(hidden_states.device):
|
| 188 |
+
_decode[(batch, triton.cdiv(channels, 32))](
|
| 189 |
+
hidden_states,
|
| 190 |
+
weight.contiguous(),
|
| 191 |
+
bias.contiguous() if bias is not None else None,
|
| 192 |
+
conv_state,
|
| 193 |
+
output,
|
| 194 |
+
channels,
|
| 195 |
+
conv_state.shape[-1],
|
| 196 |
+
*hidden_states.stride()[:2],
|
| 197 |
+
*conv_state.stride(),
|
| 198 |
+
bias is not None,
|
| 199 |
+
32,
|
| 200 |
+
enable_fp_fusion=False,
|
| 201 |
+
)
|
| 202 |
+
return output
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:510de08fc53fed5cafed593ee8c058cedf2339546679980fc5de033fce5dd995
|
| 3 |
+
size 11635872
|
modeling_lfm3.py
ADDED
|
@@ -0,0 +1,882 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import copy
|
| 15 |
+
from collections.abc import Callable
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from torch import nn
|
| 20 |
+
|
| 21 |
+
from transformers import initialization as init
|
| 22 |
+
from transformers.activations import ACT2FN
|
| 23 |
+
from transformers.cache_utils import Cache, DynamicCache
|
| 24 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 25 |
+
from transformers.generation import GenerationMixin
|
| 26 |
+
from transformers.integrations import (
|
| 27 |
+
use_kernel_forward_from_hub,
|
| 28 |
+
use_kernel_func_from_hub_with_fallback,
|
| 29 |
+
use_kernelized_func,
|
| 30 |
+
)
|
| 31 |
+
from transformers.integrations.accelerate import force_accelerate_hooks
|
| 32 |
+
from transformers.masking_utils import (
|
| 33 |
+
create_causal_mask,
|
| 34 |
+
create_recurrent_attention_mask,
|
| 35 |
+
create_sliding_window_causal_mask,
|
| 36 |
+
)
|
| 37 |
+
from transformers.modeling_layers import GradientCheckpointingLayer
|
| 38 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
|
| 39 |
+
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
|
| 40 |
+
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
|
| 41 |
+
from transformers.processing_utils import Unpack
|
| 42 |
+
from transformers.utils import TransformersKwargs, auto_docstring, can_return_tuple, torch_compilable_check
|
| 43 |
+
from transformers.utils.generic import merge_with_config_defaults
|
| 44 |
+
from transformers.utils.import_utils import is_tracing
|
| 45 |
+
from transformers.utils.output_capturing import capture_outputs
|
| 46 |
+
|
| 47 |
+
from .configuration_lfm3 import Lfm3Config
|
| 48 |
+
from .lfm3_conv import can_use_lfm3_conv, lfm3_causal_conv1d, lfm3_causal_conv1d_update
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@use_kernel_forward_from_hub("RMSNorm")
|
| 52 |
+
class Lfm3RMSNorm(nn.Module):
|
| 53 |
+
def __init__(self, hidden_size, eps: float = 1e-6) -> None:
|
| 54 |
+
"""
|
| 55 |
+
Lfm3RMSNorm is equivalent to T5LayerNorm
|
| 56 |
+
"""
|
| 57 |
+
super().__init__()
|
| 58 |
+
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 59 |
+
self.variance_epsilon = eps
|
| 60 |
+
|
| 61 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 62 |
+
input_dtype = hidden_states.dtype
|
| 63 |
+
hidden_states = hidden_states.to(torch.float32)
|
| 64 |
+
variance = hidden_states.pow(2).mean(-1, keepdim=True)
|
| 65 |
+
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
|
| 66 |
+
return self.weight * hidden_states.to(input_dtype)
|
| 67 |
+
|
| 68 |
+
def extra_repr(self):
|
| 69 |
+
return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class Lfm3RotaryEmbedding(nn.Module):
|
| 73 |
+
"""
|
| 74 |
+
Rotary embedding per attention layer type, like Gemma 3; `conv` layers register no buffers. Partial RoPE keeps the
|
| 75 |
+
frequency progression of the full head dimension and zeroes the tail instead of shrinking the rotated dimension.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
def __init__(self, config: Lfm3Config, device=None):
|
| 79 |
+
super().__init__()
|
| 80 |
+
self.max_seq_len_cached = config.max_position_embeddings
|
| 81 |
+
self.original_max_seq_len = config.max_position_embeddings
|
| 82 |
+
self.config = config
|
| 83 |
+
self.layer_types = sorted(set(config.layer_types) & set(config.rope_parameters))
|
| 84 |
+
self.rope_type = {}
|
| 85 |
+
for layer_type in self.layer_types:
|
| 86 |
+
rope_params = self.config.rope_parameters[layer_type]
|
| 87 |
+
self.rope_type[layer_type] = rope_params["rope_type"]
|
| 88 |
+
curr_inv_freq, curr_attention_scaling = self.compute_rope_parameters(config, device, layer_type=layer_type)
|
| 89 |
+
setattr(self, f"{layer_type}_inv_freq", nn.Buffer(curr_inv_freq, persistent=False))
|
| 90 |
+
setattr(self, f"{layer_type}_original_inv_freq", nn.Buffer(curr_inv_freq.clone(), persistent=False))
|
| 91 |
+
setattr(self, f"{layer_type}_attention_scaling", curr_attention_scaling)
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
def compute_default_rope_parameters(
|
| 95 |
+
config: Lfm3Config, device=None, layer_type: str | None = None, **kwargs
|
| 96 |
+
) -> tuple[torch.Tensor, float]:
|
| 97 |
+
"""
|
| 98 |
+
Inverse frequencies of the default RoPE with LFM3's zero-tail partial rotation: the first
|
| 99 |
+
`partial_rotary_factor * head_dim // 2` frequencies follow the progression of the full head dimension and the
|
| 100 |
+
rest are zero, leaving those dimensions unrotated.
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
Tuple of (`torch.Tensor`, `float`): the inverse frequencies and the attention scaling factor (always 1).
|
| 104 |
+
"""
|
| 105 |
+
base = config.rope_parameters[layer_type]["rope_theta"]
|
| 106 |
+
partial_rotary_factor = config.rope_parameters[layer_type].get("partial_rotary_factor", 1.0)
|
| 107 |
+
dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads
|
| 108 |
+
rope_angles = int(partial_rotary_factor * dim) // 2
|
| 109 |
+
inv_freq = 1.0 / (base ** (torch.arange(0, 2 * rope_angles, 2, dtype=torch.float) / dim))
|
| 110 |
+
inv_freq = torch.cat((inv_freq, torch.zeros(dim // 2 - rope_angles, dtype=torch.float)))
|
| 111 |
+
return inv_freq.to(device), 1.0
|
| 112 |
+
|
| 113 |
+
@torch.no_grad()
|
| 114 |
+
@dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope)
|
| 115 |
+
def forward(self, x, position_ids, layer_type):
|
| 116 |
+
inv_freq = getattr(self, f"{layer_type}_inv_freq")
|
| 117 |
+
attention_scaling = getattr(self, f"{layer_type}_attention_scaling")
|
| 118 |
+
|
| 119 |
+
freqs = position_ids[..., None].float() * inv_freq.to(device=x.device, dtype=torch.float)
|
| 120 |
+
emb = torch.cat((freqs, freqs), dim=-1)
|
| 121 |
+
cos = emb.cos() * attention_scaling
|
| 122 |
+
sin = emb.sin() * attention_scaling
|
| 123 |
+
|
| 124 |
+
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
|
| 125 |
+
|
| 126 |
+
@classmethod
|
| 127 |
+
def compute_rope_parameters(
|
| 128 |
+
cls, config: Lfm3Config, device=None, layer_type: str | None = None
|
| 129 |
+
) -> tuple[torch.Tensor, float]:
|
| 130 |
+
"""Inverse frequencies of any RoPE type in the zero-tail layout: scaled types are evaluated over the full head
|
| 131 |
+
dimension and truncated to the rotated prefix."""
|
| 132 |
+
rope_params = config.rope_parameters[layer_type]
|
| 133 |
+
if rope_params["rope_type"] == "default":
|
| 134 |
+
return cls.compute_default_rope_parameters(config, device, layer_type=layer_type)
|
| 135 |
+
full_config = copy.copy(config)
|
| 136 |
+
full_config.rope_parameters = {
|
| 137 |
+
**config.rope_parameters,
|
| 138 |
+
layer_type: {**rope_params, "partial_rotary_factor": 1.0},
|
| 139 |
+
}
|
| 140 |
+
inv_freq, attention_factor = ROPE_INIT_FUNCTIONS[rope_params["rope_type"]](
|
| 141 |
+
full_config, device, layer_type=layer_type
|
| 142 |
+
)
|
| 143 |
+
dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads
|
| 144 |
+
rope_angles = int(rope_params.get("partial_rotary_factor", 1.0) * dim) // 2
|
| 145 |
+
inv_freq = torch.cat((inv_freq[:rope_angles], inv_freq.new_zeros(dim // 2 - rope_angles)))
|
| 146 |
+
return inv_freq, attention_factor
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class Lfm3MLP(nn.Module):
|
| 150 |
+
def __init__(self, config: Lfm3Config):
|
| 151 |
+
super().__init__()
|
| 152 |
+
self.w1 = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
| 153 |
+
self.w3 = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
| 154 |
+
self.w2 = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
|
| 155 |
+
|
| 156 |
+
def forward(self, x):
|
| 157 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def rotate_half(x):
|
| 161 |
+
"""Rotates half the hidden dims of the input."""
|
| 162 |
+
x1 = x[..., : x.shape[-1] // 2]
|
| 163 |
+
x2 = x[..., x.shape[-1] // 2 :]
|
| 164 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
|
| 168 |
+
"""
|
| 169 |
+
This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
|
| 170 |
+
num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
|
| 171 |
+
"""
|
| 172 |
+
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
|
| 173 |
+
if n_rep == 1:
|
| 174 |
+
return hidden_states
|
| 175 |
+
hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
|
| 176 |
+
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def eager_attention_forward(
|
| 180 |
+
module: nn.Module,
|
| 181 |
+
query: torch.Tensor,
|
| 182 |
+
key: torch.Tensor,
|
| 183 |
+
value: torch.Tensor,
|
| 184 |
+
attention_mask: torch.Tensor | None,
|
| 185 |
+
scaling: float,
|
| 186 |
+
dropout: float = 0.0,
|
| 187 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 188 |
+
):
|
| 189 |
+
key_states = repeat_kv(key, module.num_key_value_groups)
|
| 190 |
+
value_states = repeat_kv(value, module.num_key_value_groups)
|
| 191 |
+
|
| 192 |
+
attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
|
| 193 |
+
if attention_mask is not None:
|
| 194 |
+
attn_weights = attn_weights + attention_mask
|
| 195 |
+
|
| 196 |
+
attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
|
| 197 |
+
attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
|
| 198 |
+
attn_output = torch.matmul(attn_weights, value_states)
|
| 199 |
+
attn_output = attn_output.transpose(1, 2).contiguous()
|
| 200 |
+
|
| 201 |
+
return attn_output, attn_weights
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1):
|
| 205 |
+
"""Applies RoPE to the query and key tensors in float32, like the reference implementation."""
|
| 206 |
+
cos = cos.unsqueeze(unsqueeze_dim).float()
|
| 207 |
+
sin = sin.unsqueeze(unsqueeze_dim).float()
|
| 208 |
+
q_embed = (q.float() * cos) + (rotate_half(q.float()) * sin)
|
| 209 |
+
k_embed = (k.float() * cos) + (rotate_half(k.float()) * sin)
|
| 210 |
+
return q_embed.to(q.dtype), k_embed.to(k.dtype)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@use_kernelized_func(apply_rotary_pos_emb)
|
| 214 |
+
class Lfm3Attention(nn.Module):
|
| 215 |
+
"""Llama attention with QK RMSNorm, a sigmoid output gate per head, a sliding window on `sliding_attention`
|
| 216 |
+
layers and RoPE in float32."""
|
| 217 |
+
|
| 218 |
+
def __init__(self, config: Lfm3Config, layer_idx: int):
|
| 219 |
+
super().__init__()
|
| 220 |
+
self.config = config
|
| 221 |
+
self.layer_idx = layer_idx
|
| 222 |
+
self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
|
| 223 |
+
self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads
|
| 224 |
+
self.scaling = self.head_dim**-0.5
|
| 225 |
+
self.is_causal = True
|
| 226 |
+
self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False)
|
| 227 |
+
self.k_proj = nn.Linear(config.hidden_size, config.num_key_value_heads * self.head_dim, bias=False)
|
| 228 |
+
self.v_proj = nn.Linear(config.hidden_size, config.num_key_value_heads * self.head_dim, bias=False)
|
| 229 |
+
self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False)
|
| 230 |
+
self.q_norm = Lfm3RMSNorm(self.head_dim, eps=config.norm_eps)
|
| 231 |
+
self.k_norm = Lfm3RMSNorm(self.head_dim, eps=config.norm_eps)
|
| 232 |
+
self.gate_proj = nn.Linear(config.hidden_size, config.num_attention_heads, bias=False)
|
| 233 |
+
self.sliding_window = config.sliding_window if config.layer_types[layer_idx] == "sliding_attention" else None
|
| 234 |
+
|
| 235 |
+
def forward(
|
| 236 |
+
self,
|
| 237 |
+
hidden_states: torch.Tensor,
|
| 238 |
+
position_embeddings: tuple[torch.Tensor, torch.Tensor],
|
| 239 |
+
attention_mask: torch.Tensor | None,
|
| 240 |
+
past_key_values: Cache | None = None,
|
| 241 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 242 |
+
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
| 243 |
+
input_shape = hidden_states.shape[:-1]
|
| 244 |
+
hidden_shape = (*input_shape, -1, self.head_dim)
|
| 245 |
+
|
| 246 |
+
query_states = self.q_norm(self.q_proj(hidden_states).view(*hidden_shape)).transpose(1, 2)
|
| 247 |
+
key_states = self.k_norm(self.k_proj(hidden_states).view(*hidden_shape)).transpose(1, 2)
|
| 248 |
+
value_states = self.v_proj(hidden_states).view(*hidden_shape).transpose(1, 2)
|
| 249 |
+
|
| 250 |
+
cos, sin = position_embeddings
|
| 251 |
+
query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
|
| 252 |
+
|
| 253 |
+
if past_key_values is not None:
|
| 254 |
+
key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx)
|
| 255 |
+
|
| 256 |
+
attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface(
|
| 257 |
+
self.config._attn_implementation, eager_attention_forward
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
attn_output, attn_weights = attention_interface(
|
| 261 |
+
self,
|
| 262 |
+
query_states,
|
| 263 |
+
key_states,
|
| 264 |
+
value_states,
|
| 265 |
+
attention_mask,
|
| 266 |
+
dropout=0.0,
|
| 267 |
+
scaling=self.scaling,
|
| 268 |
+
sliding_window=self.sliding_window,
|
| 269 |
+
**kwargs,
|
| 270 |
+
)
|
| 271 |
+
gate = torch.sigmoid(self.gate_proj(hidden_states)).unsqueeze(-1)
|
| 272 |
+
attn_output = (attn_output * gate).reshape(*input_shape, -1).contiguous()
|
| 273 |
+
attn_output = self.o_proj(attn_output)
|
| 274 |
+
return attn_output, attn_weights
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def apply_mask_to_padding_states(hidden_states, attention_mask):
|
| 278 |
+
"""
|
| 279 |
+
Tunes out the hidden states for padding tokens, see https://github.com/state-spaces/mamba/issues/66
|
| 280 |
+
"""
|
| 281 |
+
# NOTE: attention mask is a 2D boolean tensor
|
| 282 |
+
if attention_mask is not None:
|
| 283 |
+
dtype = hidden_states.dtype
|
| 284 |
+
hidden_states = (hidden_states * attention_mask[:, :, None]).to(dtype)
|
| 285 |
+
|
| 286 |
+
return hidden_states
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
@use_kernel_func_from_hub_with_fallback("causal_conv1d_update", "causal_conv1d")
|
| 290 |
+
def causal_conv1d_update(
|
| 291 |
+
hidden_states: torch.Tensor,
|
| 292 |
+
conv_state: torch.Tensor,
|
| 293 |
+
weight: nn.Parameter,
|
| 294 |
+
bias: nn.Parameter | None = None,
|
| 295 |
+
activation: str | None = None,
|
| 296 |
+
):
|
| 297 |
+
_, hidden_size, seq_len = hidden_states.shape
|
| 298 |
+
state_len = conv_state.shape[-1]
|
| 299 |
+
|
| 300 |
+
hidden_states_new = torch.cat([conv_state, hidden_states], dim=-1).to(weight.dtype)
|
| 301 |
+
conv_state.copy_(hidden_states_new[:, :, -state_len:])
|
| 302 |
+
out = F.conv1d(hidden_states_new, weight.unsqueeze(1), bias, padding=0, groups=hidden_size)
|
| 303 |
+
out = out[:, :, -seq_len:]
|
| 304 |
+
if activation is not None:
|
| 305 |
+
out = ACT2FN[activation](out)
|
| 306 |
+
return out.to(hidden_states.dtype)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@use_kernel_func_from_hub_with_fallback("causal_conv1d_fn", "causal_conv1d")
|
| 310 |
+
def causal_conv1d_fn(
|
| 311 |
+
hidden_states: torch.Tensor,
|
| 312 |
+
weight: nn.Parameter,
|
| 313 |
+
bias: nn.Parameter | None = None,
|
| 314 |
+
activation: str | None = None,
|
| 315 |
+
**kwargs,
|
| 316 |
+
):
|
| 317 |
+
_, hidden_size, seq_len = hidden_states.shape
|
| 318 |
+
padding = weight.shape[-1] - 1
|
| 319 |
+
|
| 320 |
+
out = F.conv1d(
|
| 321 |
+
hidden_states.to(weight.dtype),
|
| 322 |
+
weight=weight.unsqueeze(1),
|
| 323 |
+
bias=bias,
|
| 324 |
+
padding=padding,
|
| 325 |
+
groups=hidden_size,
|
| 326 |
+
)[:, :, :seq_len]
|
| 327 |
+
if activation is not None:
|
| 328 |
+
out = ACT2FN[activation](out)
|
| 329 |
+
return out.to(hidden_states.dtype)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
@use_kernelized_func([causal_conv1d_update, causal_conv1d_fn])
|
| 333 |
+
class Lfm3ShortConv(nn.Module):
|
| 334 |
+
"""LFM2 gated short convolution with packed-document boundaries (`seq_idx`), the padding mask re-applied after a
|
| 335 |
+
biased `in_proj`, and width-7 Triton inference kernels with a PyTorch fallback."""
|
| 336 |
+
|
| 337 |
+
def __init__(
|
| 338 |
+
self,
|
| 339 |
+
config: Lfm3Config,
|
| 340 |
+
layer_idx: int,
|
| 341 |
+
):
|
| 342 |
+
super().__init__()
|
| 343 |
+
self.config = config
|
| 344 |
+
self.layer_idx = layer_idx
|
| 345 |
+
self.conv_kernel_size = config.conv_L_cache
|
| 346 |
+
self.bias = config.conv_bias
|
| 347 |
+
|
| 348 |
+
self.conv = nn.Conv1d(
|
| 349 |
+
in_channels=config.hidden_size,
|
| 350 |
+
out_channels=config.hidden_size,
|
| 351 |
+
kernel_size=self.conv_kernel_size,
|
| 352 |
+
groups=config.hidden_size,
|
| 353 |
+
bias=self.bias,
|
| 354 |
+
padding=self.conv_kernel_size - 1,
|
| 355 |
+
)
|
| 356 |
+
self.in_proj = nn.Linear(config.hidden_size, 3 * config.hidden_size, bias=self.bias)
|
| 357 |
+
self.out_proj = nn.Linear(config.hidden_size, config.hidden_size, bias=self.bias)
|
| 358 |
+
|
| 359 |
+
self.layer_type = config.layer_types[layer_idx]
|
| 360 |
+
|
| 361 |
+
@force_accelerate_hooks("conv")
|
| 362 |
+
def forward(
|
| 363 |
+
self,
|
| 364 |
+
hidden_states: torch.Tensor,
|
| 365 |
+
past_key_values: Cache | None = None,
|
| 366 |
+
attention_mask: torch.Tensor | None = None,
|
| 367 |
+
seq_idx: torch.IntTensor | None = None,
|
| 368 |
+
):
|
| 369 |
+
seq_len = hidden_states.shape[1]
|
| 370 |
+
|
| 371 |
+
hidden_states = apply_mask_to_padding_states(hidden_states, attention_mask)
|
| 372 |
+
BCx = self.in_proj(hidden_states).transpose(-1, -2)
|
| 373 |
+
B, C, x = BCx.chunk(3, dim=-2)
|
| 374 |
+
hidden_states = B * x
|
| 375 |
+
if attention_mask is not None:
|
| 376 |
+
# with `conv_bias` the projection makes the padded positions non-zero again
|
| 377 |
+
hidden_states = hidden_states * attention_mask[:, None, :].to(hidden_states.dtype)
|
| 378 |
+
|
| 379 |
+
use_precomputed_states = past_key_values is not None and past_key_values.has_previous_state(self.layer_idx)
|
| 380 |
+
use_kernels = 2 <= self.conv_kernel_size <= 4
|
| 381 |
+
use_wide_kernel = can_use_lfm3_conv(hidden_states, self.conv.weight)
|
| 382 |
+
|
| 383 |
+
if use_precomputed_states and seq_len == 1 and not past_key_values.layers[self.layer_idx].record_past:
|
| 384 |
+
conv_state = past_key_values.layers[self.layer_idx].conv_states[0]
|
| 385 |
+
if use_wide_kernel and conv_state.shape[-1] in (6, 7):
|
| 386 |
+
hidden_states = lfm3_causal_conv1d_update(
|
| 387 |
+
hidden_states, conv_state, self.conv.weight.squeeze(1), self.conv.bias
|
| 388 |
+
)
|
| 389 |
+
elif use_kernels:
|
| 390 |
+
hidden_states = causal_conv1d_update(
|
| 391 |
+
hidden_states, conv_state, self.conv.weight.squeeze(1), self.conv.bias
|
| 392 |
+
)
|
| 393 |
+
else:
|
| 394 |
+
hidden_states = torch.cat([conv_state, hidden_states], dim=-1)
|
| 395 |
+
conv_state.copy_(hidden_states[:, :, -conv_state.shape[-1] :])
|
| 396 |
+
hidden_states = self._conv(hidden_states)[:, :, -seq_len:]
|
| 397 |
+
else:
|
| 398 |
+
if past_key_values is not None and seq_idx is not None:
|
| 399 |
+
# `seq_idx` is only set on an empty cache, and the decode steps read the cached window without it:
|
| 400 |
+
# only the last document (after any positional left padding) may enter the window
|
| 401 |
+
last_document = (seq_idx == seq_idx[:, -1:])[:, None].to(hidden_states.dtype)
|
| 402 |
+
past_key_values.update_conv_state(
|
| 403 |
+
hidden_states * last_document, self.layer_idx, conv_kernel_size=self.conv_kernel_size
|
| 404 |
+
)
|
| 405 |
+
elif past_key_values is not None:
|
| 406 |
+
hidden_states = past_key_values.update_conv_state(
|
| 407 |
+
hidden_states, self.layer_idx, conv_kernel_size=self.conv_kernel_size
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
if use_wide_kernel:
|
| 411 |
+
hidden_states = lfm3_causal_conv1d(hidden_states, self.conv.weight.squeeze(1), self.conv.bias, seq_idx)
|
| 412 |
+
elif seq_idx is not None:
|
| 413 |
+
hidden_states = self._segmented_conv(hidden_states, seq_idx)
|
| 414 |
+
elif use_kernels:
|
| 415 |
+
hidden_states = causal_conv1d_fn(hidden_states, self.conv.weight.squeeze(1), self.conv.bias)
|
| 416 |
+
else:
|
| 417 |
+
hidden_states = self._conv(hidden_states)
|
| 418 |
+
|
| 419 |
+
# Drop the additional previous states
|
| 420 |
+
if past_key_values is not None:
|
| 421 |
+
hidden_states = hidden_states[:, :, -seq_len:]
|
| 422 |
+
|
| 423 |
+
y = C * hidden_states
|
| 424 |
+
y = y.transpose(-1, -2).contiguous()
|
| 425 |
+
y = self.out_proj(y)
|
| 426 |
+
return y
|
| 427 |
+
|
| 428 |
+
def _conv(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 429 |
+
"""Causal depthwise conv in torch: the `causal_conv1d` kernels only accept widths 2 to 4."""
|
| 430 |
+
conv_out = self.conv(hidden_states.to(self.conv.weight.dtype))[..., : hidden_states.shape[-1]]
|
| 431 |
+
return conv_out.to(hidden_states.dtype)
|
| 432 |
+
|
| 433 |
+
def _segmented_conv(self, hidden_states: torch.Tensor, seq_idx: torch.Tensor) -> torch.Tensor:
|
| 434 |
+
"""Causal depthwise conv that only reads positions sharing the current token's document index."""
|
| 435 |
+
kernel_size = self.conv_kernel_size
|
| 436 |
+
seq_idx = F.pad(seq_idx, (hidden_states.shape[-1] - seq_idx.shape[-1] + kernel_size - 1, 0), value=-1)
|
| 437 |
+
windows = F.pad(hidden_states, (kernel_size - 1, 0)).unfold(-1, kernel_size, 1)
|
| 438 |
+
seq_idx_windows = seq_idx.unfold(-1, kernel_size, 1)
|
| 439 |
+
same_document = (seq_idx_windows == seq_idx_windows[..., -1:]).unsqueeze(1)
|
| 440 |
+
conv_out = torch.einsum("bdtk,dk->bdt", windows * same_document.to(windows.dtype), self.conv.weight.squeeze(1))
|
| 441 |
+
if self.bias:
|
| 442 |
+
conv_out = conv_out + self.conv.bias[None, :, None]
|
| 443 |
+
return conv_out.to(hidden_states.dtype)
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
class Lfm3DecoderLayer(GradientCheckpointingLayer):
|
| 447 |
+
def __init__(self, config: Lfm3Config, layer_idx: int):
|
| 448 |
+
super().__init__()
|
| 449 |
+
self.is_attention_layer = config.layer_types[layer_idx] in config._attention_layer_types
|
| 450 |
+
self.use_sandwich_norm = config.use_sandwich_norm
|
| 451 |
+
|
| 452 |
+
if self.is_attention_layer:
|
| 453 |
+
self.self_attn = Lfm3Attention(config, layer_idx)
|
| 454 |
+
else:
|
| 455 |
+
self.conv = Lfm3ShortConv(config, layer_idx)
|
| 456 |
+
self.feed_forward = Lfm3MLP(config)
|
| 457 |
+
self.operator_norm = Lfm3RMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 458 |
+
self.ffn_norm = Lfm3RMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 459 |
+
if self.use_sandwich_norm:
|
| 460 |
+
self.post_operator_norm = Lfm3RMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 461 |
+
self.post_ffn_norm = Lfm3RMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 462 |
+
|
| 463 |
+
def forward(
|
| 464 |
+
self,
|
| 465 |
+
hidden_states: torch.Tensor,
|
| 466 |
+
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
|
| 467 |
+
attention_mask: torch.Tensor | None = None,
|
| 468 |
+
position_ids: torch.LongTensor | None = None,
|
| 469 |
+
past_key_values: Cache | None = None,
|
| 470 |
+
seq_idx: torch.IntTensor | None = None,
|
| 471 |
+
**kwargs,
|
| 472 |
+
) -> torch.Tensor:
|
| 473 |
+
residual = hidden_states
|
| 474 |
+
if self.is_attention_layer:
|
| 475 |
+
hidden_states, _ = self.self_attn(
|
| 476 |
+
hidden_states=self.operator_norm(hidden_states),
|
| 477 |
+
position_embeddings=position_embeddings,
|
| 478 |
+
attention_mask=attention_mask,
|
| 479 |
+
position_ids=position_ids,
|
| 480 |
+
past_key_values=past_key_values,
|
| 481 |
+
**kwargs,
|
| 482 |
+
)
|
| 483 |
+
else:
|
| 484 |
+
hidden_states = self.conv(
|
| 485 |
+
hidden_states=self.operator_norm(hidden_states),
|
| 486 |
+
past_key_values=past_key_values,
|
| 487 |
+
attention_mask=attention_mask,
|
| 488 |
+
seq_idx=seq_idx,
|
| 489 |
+
)
|
| 490 |
+
if self.use_sandwich_norm:
|
| 491 |
+
hidden_states = self.post_operator_norm(hidden_states)
|
| 492 |
+
hidden_states = hidden_states + residual
|
| 493 |
+
|
| 494 |
+
ffn_out = self.feed_forward(self.ffn_norm(hidden_states))
|
| 495 |
+
if self.use_sandwich_norm:
|
| 496 |
+
ffn_out = self.post_ffn_norm(ffn_out)
|
| 497 |
+
hidden_states = hidden_states + ffn_out
|
| 498 |
+
|
| 499 |
+
return hidden_states
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def create_masks_by_layer_type(
|
| 503 |
+
config: PreTrainedConfig,
|
| 504 |
+
inputs_embeds: torch.Tensor,
|
| 505 |
+
attention_mask: torch.Tensor | None,
|
| 506 |
+
past_key_values: Cache | None,
|
| 507 |
+
position_ids: torch.Tensor | None = None,
|
| 508 |
+
) -> dict:
|
| 509 |
+
"""The cache sizes each mask from the layer it is asked about, and the generic mask utilities ask about the first
|
| 510 |
+
non-sliding layer, which for a leading `conv` layer redirects to the first attention layer of any type: name the
|
| 511 |
+
first layer of each mask's own type instead."""
|
| 512 |
+
config = config.get_text_config()
|
| 513 |
+
mask_kwargs = {
|
| 514 |
+
"config": config,
|
| 515 |
+
"inputs_embeds": inputs_embeds,
|
| 516 |
+
"attention_mask": attention_mask,
|
| 517 |
+
"past_key_values": past_key_values,
|
| 518 |
+
"position_ids": position_ids,
|
| 519 |
+
}
|
| 520 |
+
layer_types, causal_mask_mapping = config.layer_types, {}
|
| 521 |
+
if "full_attention" in layer_types:
|
| 522 |
+
layer_idx = layer_types.index("full_attention")
|
| 523 |
+
causal_mask_mapping["full_attention"] = create_causal_mask(**mask_kwargs, layer_idx=layer_idx)
|
| 524 |
+
if "sliding_attention" in layer_types:
|
| 525 |
+
layer_idx = layer_types.index("sliding_attention")
|
| 526 |
+
causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(
|
| 527 |
+
**mask_kwargs, layer_idx=layer_idx
|
| 528 |
+
)
|
| 529 |
+
if "conv" in layer_types:
|
| 530 |
+
causal_mask_mapping["conv"] = create_recurrent_attention_mask(**mask_kwargs)
|
| 531 |
+
return causal_mask_mapping
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
@auto_docstring
|
| 535 |
+
class Lfm3PreTrainedModel(PreTrainedModel):
|
| 536 |
+
config: Lfm3Config
|
| 537 |
+
base_model_prefix = "model"
|
| 538 |
+
supports_gradient_checkpointing = True
|
| 539 |
+
_no_split_modules = ["Lfm3DecoderLayer"]
|
| 540 |
+
_skip_keys_device_placement = ["past_key_values"]
|
| 541 |
+
_supports_flash_attn = True
|
| 542 |
+
_supports_sdpa = True
|
| 543 |
+
_supports_flex_attn = True
|
| 544 |
+
|
| 545 |
+
_can_compile_fullgraph = True
|
| 546 |
+
_supports_attention_backend = True
|
| 547 |
+
_can_record_outputs = {
|
| 548 |
+
"hidden_states": Lfm3DecoderLayer,
|
| 549 |
+
"attentions": Lfm3Attention,
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
@torch.no_grad()
|
| 553 |
+
def _init_weights(self, module):
|
| 554 |
+
super()._init_weights(module)
|
| 555 |
+
if isinstance(module, Lfm3RotaryEmbedding):
|
| 556 |
+
for layer_type in module.layer_types:
|
| 557 |
+
curr_inv_freq, _ = module.compute_rope_parameters(module.config, layer_type=layer_type)
|
| 558 |
+
init.copy_(getattr(module, f"{layer_type}_inv_freq"), curr_inv_freq)
|
| 559 |
+
init.copy_(getattr(module, f"{layer_type}_original_inv_freq"), curr_inv_freq)
|
| 560 |
+
|
| 561 |
+
@staticmethod
|
| 562 |
+
def create_masks_for_generate(
|
| 563 |
+
config: PreTrainedConfig,
|
| 564 |
+
inputs_embeds: torch.Tensor,
|
| 565 |
+
attention_mask: torch.Tensor | None,
|
| 566 |
+
past_key_values: Cache | None,
|
| 567 |
+
position_ids: torch.Tensor | None = None,
|
| 568 |
+
**kwargs,
|
| 569 |
+
) -> dict:
|
| 570 |
+
return create_masks_by_layer_type(config, inputs_embeds, attention_mask, past_key_values, position_ids)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def packed_under_padding(position_ids: torch.Tensor, padding_mask: torch.Tensor, filled) -> torch.Tensor:
|
| 574 |
+
"""Padding repeats position 0, so packing is a restart between two kept tokens, or a kept position 0 on a filled
|
| 575 |
+
cache whose row holds a kept token; attention under a padding mask cannot isolate it."""
|
| 576 |
+
restarts = torch.diff(position_ids, prepend=position_ids[:, :1] - 1, dim=-1) != 1
|
| 577 |
+
valid = padding_mask[:, -restarts.shape[-1] :].bool()
|
| 578 |
+
packed = restarts[:, 1:] & valid[:, 1:] & valid[:, :-1]
|
| 579 |
+
if filled:
|
| 580 |
+
# a row whose cached tokens are all padding (chunked prefill of a left-padded batch) has no document to pack
|
| 581 |
+
# against; a mask covering only the chunk keeps the strict rule
|
| 582 |
+
cached = padding_mask[:, : padding_mask.shape[-1] - restarts.shape[-1]]
|
| 583 |
+
cached_valid = cached.bool().any(-1, keepdim=True) if cached.shape[-1] else torch.ones_like(valid[:, :1])
|
| 584 |
+
packed = packed | (valid[:, :1] & (position_ids[:, :1] == 0) & cached_valid)
|
| 585 |
+
return packed
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
@auto_docstring
|
| 589 |
+
class Lfm3Model(Lfm3PreTrainedModel):
|
| 590 |
+
"""LFM2 model with sliding-attention layers as a third layer type: the mask and the rotary embedding are built per
|
| 591 |
+
layer type, and packed documents (`position_ids` restarting) reset the short-conv window through `seq_idx`."""
|
| 592 |
+
|
| 593 |
+
def __init__(self, config: Lfm3Config):
|
| 594 |
+
super().__init__(config)
|
| 595 |
+
self.padding_idx = config.pad_token_id
|
| 596 |
+
self.vocab_size = config.vocab_size
|
| 597 |
+
|
| 598 |
+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
|
| 599 |
+
self.layers = nn.ModuleList(
|
| 600 |
+
[Lfm3DecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
|
| 601 |
+
)
|
| 602 |
+
self.rotary_emb = Lfm3RotaryEmbedding(config=config)
|
| 603 |
+
self.gradient_checkpointing = False
|
| 604 |
+
self.embedding_norm = Lfm3RMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 605 |
+
|
| 606 |
+
# Initialize weights and apply final processing
|
| 607 |
+
self.post_init()
|
| 608 |
+
|
| 609 |
+
@merge_with_config_defaults
|
| 610 |
+
@capture_outputs
|
| 611 |
+
@auto_docstring
|
| 612 |
+
def forward(
|
| 613 |
+
self,
|
| 614 |
+
input_ids: torch.LongTensor | None = None,
|
| 615 |
+
attention_mask: torch.Tensor | None = None,
|
| 616 |
+
position_ids: torch.LongTensor | None = None,
|
| 617 |
+
past_key_values: Cache | None = None,
|
| 618 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 619 |
+
use_cache: bool | None = None,
|
| 620 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 621 |
+
) -> BaseModelOutputWithPast:
|
| 622 |
+
r"""
|
| 623 |
+
attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)` or `dict`, *optional*):
|
| 624 |
+
Mask to avoid performing attention on padding token indices (1 keeps a token, 0 masks it), or the mask
|
| 625 |
+
dict of `create_masks_for_generate`. A mask dict is used as given: `generate` refuses packed
|
| 626 |
+
`position_ids` before its prefill.
|
| 627 |
+
"""
|
| 628 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 629 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 630 |
+
# `seq_idx` is a shared kwarg (`DataCollatorWithFlattening(return_seq_idx=True)`, read by Lfm2); LFM3 derives
|
| 631 |
+
# the documents from the restarting `position_ids` that come with it
|
| 632 |
+
if kwargs.pop("seq_idx", None) is not None and position_ids is None:
|
| 633 |
+
raise ValueError("Pass packed documents as `position_ids` restarting at 0; LFM3 does not read `seq_idx`")
|
| 634 |
+
|
| 635 |
+
if inputs_embeds is None:
|
| 636 |
+
inputs_embeds = self.embed_tokens(input_ids)
|
| 637 |
+
|
| 638 |
+
if use_cache and past_key_values is None:
|
| 639 |
+
past_key_values = DynamicCache(config=self.config)
|
| 640 |
+
filled = past_key_values is not None and (
|
| 641 |
+
past_key_values.get_seq_length() > 0
|
| 642 |
+
if any(t != "conv" for t in self.config.layer_types)
|
| 643 |
+
else past_key_values.has_previous_state()
|
| 644 |
+
)
|
| 645 |
+
packed_cache = getattr(past_key_values, "lfm3_packed", None)
|
| 646 |
+
if isinstance(packed_cache, torch.Tensor) or packed_cache:
|
| 647 |
+
# Reset layers have no history to mix. Ordinary caches carry a Python `False` and skip this, so the decode
|
| 648 |
+
# steps do no tensor work here; only a compiled packed prefill leaves a tensor flag, and `filled` stays
|
| 649 |
+
# tensor-valued for static caches.
|
| 650 |
+
packed_message = (
|
| 651 |
+
"A cache filled by a packed-document forward cannot be continued: its attention spans the documents"
|
| 652 |
+
)
|
| 653 |
+
packed_cache = packed_cache & filled if isinstance(packed_cache, torch.Tensor) else filled
|
| 654 |
+
if isinstance(packed_cache, torch.Tensor):
|
| 655 |
+
torch_compilable_check(~packed_cache, packed_message)
|
| 656 |
+
if not is_tracing(packed_cache):
|
| 657 |
+
past_key_values.lfm3_packed = bool(packed_cache)
|
| 658 |
+
elif packed_cache:
|
| 659 |
+
raise ValueError(packed_message)
|
| 660 |
+
else:
|
| 661 |
+
past_key_values.lfm3_packed = False
|
| 662 |
+
|
| 663 |
+
# The mask utilities detect packing only with `attention_mask=None`, so a multi-token forward with explicit
|
| 664 |
+
# positions drops the all-ones mask tokenizers emit (a data-dependent check, hence not when traced).
|
| 665 |
+
explicit_positions = position_ids is not None and inputs_embeds.shape[1] > 1
|
| 666 |
+
if (
|
| 667 |
+
explicit_positions
|
| 668 |
+
and isinstance(attention_mask, torch.Tensor)
|
| 669 |
+
and attention_mask.ndim == 2
|
| 670 |
+
and not is_tracing(position_ids)
|
| 671 |
+
and bool(attention_mask.all())
|
| 672 |
+
):
|
| 673 |
+
attention_mask = None
|
| 674 |
+
seq_idx = None
|
| 675 |
+
if position_ids is None:
|
| 676 |
+
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 677 |
+
position_ids = torch.arange(inputs_embeds.shape[1], device=inputs_embeds.device) + past_seen_tokens
|
| 678 |
+
position_ids = position_ids.unsqueeze(0)
|
| 679 |
+
elif explicit_positions and not isinstance(attention_mask, dict):
|
| 680 |
+
# a mask dict comes from `generate`, which refused packing before its prefill
|
| 681 |
+
if isinstance(attention_mask, torch.Tensor) and attention_mask.ndim == 2:
|
| 682 |
+
# data-dependent: a traced forward skips it (`generate` refuses packing eagerly before its prefill)
|
| 683 |
+
if not is_tracing(position_ids):
|
| 684 |
+
torch_compilable_check(
|
| 685 |
+
~packed_under_padding(position_ids, attention_mask, filled),
|
| 686 |
+
"Packed documents (`position_ids` restarting) cannot be combined with an attention mask: "
|
| 687 |
+
"attention would not isolate the documents. Pass `attention_mask=None` without a static cache.",
|
| 688 |
+
)
|
| 689 |
+
else:
|
| 690 |
+
restarts = torch.diff(position_ids, prepend=position_ids[:, :1] - 1, dim=-1) != 1
|
| 691 |
+
document_ids = restarts.cumsum(-1)
|
| 692 |
+
if is_tracing(position_ids) or bool((document_ids[..., -1] > 0).any()):
|
| 693 |
+
seq_idx = document_ids.to(torch.int32)
|
| 694 |
+
# Packed documents are prefilled in one forward on an empty cache (cached tokens would join the first
|
| 695 |
+
# document), and their cache is never continued
|
| 696 |
+
if filled:
|
| 697 |
+
torch_compilable_check(
|
| 698 |
+
(~restarts[:, 1:]).all() & (position_ids[:, 0] != 0).all(),
|
| 699 |
+
"Packed documents must be prefilled in one forward; the cache already holds tokens",
|
| 700 |
+
)
|
| 701 |
+
# Ordinary continuation must retain the convolution window from the cached document.
|
| 702 |
+
seq_idx = None
|
| 703 |
+
if attention_mask is None:
|
| 704 |
+
# without a mask only the cacheless packed-sequence mask isolates repeated zero positions (left
|
| 705 |
+
# padding): a cache-sized mask and every later decode step would attend them, so they count too
|
| 706 |
+
packed = (document_ids[..., -1] > 0).any()
|
| 707 |
+
else:
|
| 708 |
+
# a prepared 4D mask is the caller's and carries the padding (repeated zero positions), but may
|
| 709 |
+
# or may not isolate documents, so the cache records the packing and is never continued
|
| 710 |
+
steps = torch.diff(position_ids, dim=-1)
|
| 711 |
+
packed = ((steps != 1) & ~((steps == 0) & (position_ids[:, :-1] == 0))).any()
|
| 712 |
+
if past_key_values is not None:
|
| 713 |
+
# resolved here, where the prefill already syncs, so the decode steps carry a Python flag: no
|
| 714 |
+
# per-step host sync, data-dependent op or CUDA-graph output on the cache
|
| 715 |
+
past_key_values.lfm3_packed = packed if is_tracing(position_ids) else bool(packed)
|
| 716 |
+
|
| 717 |
+
if not isinstance(causal_mask_mapping := attention_mask, dict):
|
| 718 |
+
mask_kwargs = {
|
| 719 |
+
"config": self.config,
|
| 720 |
+
"inputs_embeds": inputs_embeds,
|
| 721 |
+
"attention_mask": attention_mask,
|
| 722 |
+
"past_key_values": past_key_values,
|
| 723 |
+
"position_ids": position_ids,
|
| 724 |
+
}
|
| 725 |
+
# The mask utilities only isolate packed documents without a cache, and an empty cache adds nothing.
|
| 726 |
+
layer_types = self.config.layer_types
|
| 727 |
+
if seq_idx is not None and past_key_values is not None:
|
| 728 |
+
attention_layer = next((i for i, t in enumerate(layer_types) if t != "conv"), None)
|
| 729 |
+
if attention_layer is not None and past_key_values.get_seq_length(attention_layer) == 0:
|
| 730 |
+
if past_key_values.is_compileable:
|
| 731 |
+
# static layers always return their full length, so the masks are sized by the cache, which
|
| 732 |
+
# isolates neither documents nor positional-only left padding
|
| 733 |
+
torch_compilable_check(
|
| 734 |
+
~packed,
|
| 735 |
+
"Packed documents, or left padding without an attention mask, cannot be prefilled into a "
|
| 736 |
+
"static cache: its masks are sized by the cache. Pass the padding as `attention_mask`, or "
|
| 737 |
+
"use a `DynamicCache` for packed documents",
|
| 738 |
+
)
|
| 739 |
+
else:
|
| 740 |
+
mask_kwargs["past_key_values"] = None
|
| 741 |
+
causal_mask_mapping = create_masks_by_layer_type(**mask_kwargs)
|
| 742 |
+
|
| 743 |
+
hidden_states = inputs_embeds
|
| 744 |
+
position_embeddings = {
|
| 745 |
+
layer_type: self.rotary_emb(hidden_states, position_ids=position_ids, layer_type=layer_type)
|
| 746 |
+
for layer_type in self.rotary_emb.layer_types
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
for i, decoder_layer in enumerate(self.layers[: self.config.num_hidden_layers]):
|
| 750 |
+
layer_type = self.config.layer_types[i]
|
| 751 |
+
hidden_states = decoder_layer(
|
| 752 |
+
hidden_states,
|
| 753 |
+
attention_mask=causal_mask_mapping[layer_type],
|
| 754 |
+
position_embeddings=position_embeddings.get(layer_type),
|
| 755 |
+
position_ids=position_ids,
|
| 756 |
+
past_key_values=past_key_values,
|
| 757 |
+
seq_idx=seq_idx,
|
| 758 |
+
**kwargs,
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
hidden_states = self.embedding_norm(hidden_states)
|
| 762 |
+
|
| 763 |
+
return BaseModelOutputWithPast(
|
| 764 |
+
last_hidden_state=hidden_states,
|
| 765 |
+
past_key_values=past_key_values,
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
def reject_packed_generation(config: PreTrainedConfig, input_ids, generation_config, model_kwargs, is_first_iteration):
|
| 770 |
+
"""`generate` decodes from the cache its prefill fills, and no mask it builds isolates packed documents
|
| 771 |
+
(`position_ids` restarting): the prompt is refused before the prefill, one forward or every chunk, writes to it."""
|
| 772 |
+
position_ids, attention_mask = model_kwargs.get("position_ids"), model_kwargs.get("attention_mask")
|
| 773 |
+
cache = model_kwargs.get("past_key_values")
|
| 774 |
+
if cache is None or position_ids is None or position_ids.ndim != 2:
|
| 775 |
+
return
|
| 776 |
+
if attention_mask is not None and attention_mask.ndim != 2:
|
| 777 |
+
return
|
| 778 |
+
has_attention = any(t != "conv" for t in config.get_text_config().layer_types)
|
| 779 |
+
filled = bool(cache.get_seq_length() > 0 if has_attention else cache.has_previous_state())
|
| 780 |
+
inputs_embeds = model_kwargs.get("inputs_embeds") if is_first_iteration else None
|
| 781 |
+
length = input_ids.shape[1] if inputs_embeds is None else inputs_embeds.shape[1]
|
| 782 |
+
if generation_config.prefill_chunk_size is not None:
|
| 783 |
+
# the chunks run from the start of the prompt
|
| 784 |
+
position_ids = position_ids[:, :length]
|
| 785 |
+
else:
|
| 786 |
+
if filled and has_attention:
|
| 787 |
+
# the slice `_prefill` feeds: a prompt that spans the cache is cut to its uncached part
|
| 788 |
+
mask_length = getattr(generation_config, "_mask_length", -1)
|
| 789 |
+
if attention_mask is not None:
|
| 790 |
+
mask_length = attention_mask.shape[1]
|
| 791 |
+
if inputs_embeds is not None or mask_length == length:
|
| 792 |
+
length -= int(cache.get_seq_length())
|
| 793 |
+
position_ids = position_ids[:, position_ids.shape[1] - length :]
|
| 794 |
+
if attention_mask is None:
|
| 795 |
+
attention_mask = torch.ones_like(position_ids)
|
| 796 |
+
if bool(packed_under_padding(position_ids, attention_mask, filled).any()):
|
| 797 |
+
raise ValueError(
|
| 798 |
+
"`generate` cannot decode packed documents (`position_ids` restarting, or starting at 0 on a cache that "
|
| 799 |
+
"holds tokens): attention would span them. Generate each document in its own row of a left-padded batch."
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
@auto_docstring
|
| 804 |
+
class Lfm3ForCausalLM(Lfm3PreTrainedModel, GenerationMixin):
|
| 805 |
+
_tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
|
| 806 |
+
_tp_plan = {"lm_head": "colwise_gather_output"}
|
| 807 |
+
_pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
|
| 808 |
+
_fsdp_plan = {"lm_head": "keep_full_weight"}
|
| 809 |
+
|
| 810 |
+
def __init__(self, config):
|
| 811 |
+
super().__init__(config)
|
| 812 |
+
self.model = Lfm3Model(config)
|
| 813 |
+
self.vocab_size = config.vocab_size
|
| 814 |
+
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
| 815 |
+
|
| 816 |
+
# Initialize weights and apply final processing
|
| 817 |
+
self.post_init()
|
| 818 |
+
|
| 819 |
+
@can_return_tuple
|
| 820 |
+
@auto_docstring
|
| 821 |
+
def forward(
|
| 822 |
+
self,
|
| 823 |
+
input_ids: torch.LongTensor | None = None,
|
| 824 |
+
attention_mask: torch.Tensor | None = None,
|
| 825 |
+
position_ids: torch.LongTensor | None = None,
|
| 826 |
+
past_key_values: Cache | None = None,
|
| 827 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 828 |
+
labels: torch.LongTensor | None = None,
|
| 829 |
+
use_cache: bool | None = None,
|
| 830 |
+
logits_to_keep: int | torch.Tensor = 0,
|
| 831 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 832 |
+
) -> CausalLMOutputWithPast:
|
| 833 |
+
r"""
|
| 834 |
+
Example:
|
| 835 |
+
|
| 836 |
+
```python
|
| 837 |
+
>>> from transformers import AutoTokenizer, Lfm3ForCausalLM
|
| 838 |
+
|
| 839 |
+
>>> model = Lfm3ForCausalLM.from_pretrained("LiquidAI/LFM3-1.7B")
|
| 840 |
+
>>> tokenizer = AutoTokenizer.from_pretrained("LiquidAI/LFM3-1.7B")
|
| 841 |
+
|
| 842 |
+
>>> inputs = tokenizer.apply_chat_template(
|
| 843 |
+
... [{"role": "user", "content": "What is C. elegans?"}], add_generation_prompt=True, return_tensors="pt"
|
| 844 |
+
... )
|
| 845 |
+
>>> generate_ids = model.generate(**inputs, max_new_tokens=64)
|
| 846 |
+
>>> tokenizer.decode(generate_ids[0, inputs["input_ids"].shape[1] :], skip_special_tokens=True)
|
| 847 |
+
```"""
|
| 848 |
+
outputs: BaseModelOutputWithPast = self.model(
|
| 849 |
+
input_ids=input_ids,
|
| 850 |
+
attention_mask=attention_mask,
|
| 851 |
+
position_ids=position_ids,
|
| 852 |
+
past_key_values=past_key_values,
|
| 853 |
+
inputs_embeds=inputs_embeds,
|
| 854 |
+
use_cache=use_cache,
|
| 855 |
+
**kwargs,
|
| 856 |
+
)
|
| 857 |
+
|
| 858 |
+
hidden_states = outputs.last_hidden_state
|
| 859 |
+
# Only compute necessary logits, and do not upcast them to float if we are not computing the loss
|
| 860 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 861 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 862 |
+
|
| 863 |
+
loss = None
|
| 864 |
+
if labels is not None:
|
| 865 |
+
loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs)
|
| 866 |
+
|
| 867 |
+
return CausalLMOutputWithPast(
|
| 868 |
+
loss=loss,
|
| 869 |
+
logits=logits,
|
| 870 |
+
past_key_values=outputs.past_key_values,
|
| 871 |
+
hidden_states=outputs.hidden_states,
|
| 872 |
+
attentions=outputs.attentions,
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
def _prefill(self, input_ids, generation_config, model_kwargs, is_first_iteration=True):
|
| 876 |
+
# every prefill passes here once before its first forward: a check per chunk would leave the earlier chunks
|
| 877 |
+
# in the cache, and a dynamic cache would only refuse packing at the first decode step
|
| 878 |
+
reject_packed_generation(self.config, input_ids, generation_config, model_kwargs, is_first_iteration)
|
| 879 |
+
return super()._prefill(input_ids, generation_config, model_kwargs, is_first_iteration=is_first_iteration)
|
| 880 |
+
|
| 881 |
+
|
| 882 |
+
__all__ = ["Lfm3ForCausalLM", "Lfm3Model", "Lfm3PreTrainedModel"]
|
modeling_lfm3_omni.py
ADDED
|
@@ -0,0 +1,1863 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import copy
|
| 15 |
+
import warnings
|
| 16 |
+
from collections import deque
|
| 17 |
+
from dataclasses import dataclass
|
| 18 |
+
from enum import Enum
|
| 19 |
+
from typing import TypedDict
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
from torch import nn
|
| 24 |
+
|
| 25 |
+
from transformers import initialization as init
|
| 26 |
+
from transformers.activations import ACT2FN
|
| 27 |
+
from transformers.cache_utils import Cache, DynamicCache
|
| 28 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 29 |
+
from transformers.generation import GenerationConfig, GenerationMixin
|
| 30 |
+
from transformers.generation.logits_process import (
|
| 31 |
+
MinPLogitsWarper,
|
| 32 |
+
RepetitionPenaltyLogitsProcessor,
|
| 33 |
+
TemperatureLogitsWarper,
|
| 34 |
+
TopKLogitsWarper,
|
| 35 |
+
TopPLogitsWarper,
|
| 36 |
+
)
|
| 37 |
+
from transformers.generation.streamers import BaseStreamer
|
| 38 |
+
from transformers.masking_utils import (
|
| 39 |
+
create_causal_mask,
|
| 40 |
+
create_recurrent_attention_mask,
|
| 41 |
+
create_sliding_window_causal_mask,
|
| 42 |
+
)
|
| 43 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
|
| 44 |
+
from transformers.modeling_utils import PreTrainedModel
|
| 45 |
+
from transformers.models.auto import AutoModel
|
| 46 |
+
from transformers.processing_utils import Unpack
|
| 47 |
+
from transformers.utils import (
|
| 48 |
+
ModelOutput,
|
| 49 |
+
TensorType,
|
| 50 |
+
TransformersKwargs,
|
| 51 |
+
auto_docstring,
|
| 52 |
+
can_return_tuple,
|
| 53 |
+
torch_compilable_check,
|
| 54 |
+
)
|
| 55 |
+
from transformers.utils.import_utils import is_tracing
|
| 56 |
+
|
| 57 |
+
from .configuration_lfm3_omni import Lfm3OmniConfig, Lfm3OmniEncoderConfig
|
| 58 |
+
from .modeling_lfm3 import Lfm3Model
|
| 59 |
+
from .modeling_lfm3_vl import Lfm3VlVisionModel
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@auto_docstring(
|
| 63 |
+
custom_intro="""
|
| 64 |
+
Base class for Lfm3Omni outputs, with hidden states and attentions.
|
| 65 |
+
"""
|
| 66 |
+
)
|
| 67 |
+
@dataclass
|
| 68 |
+
class Lfm3OmniModelOutputWithPast(BaseModelOutputWithPast):
|
| 69 |
+
r"""
|
| 70 |
+
past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`):
|
| 71 |
+
It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache).
|
| 72 |
+
Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see
|
| 73 |
+
`past_key_values` input) to speed up sequential decoding.
|
| 74 |
+
audio_hidden_states (`torch.FloatTensor`, *optional*):
|
| 75 |
+
A `torch.FloatTensor` of size `(num_audio_tokens, hidden_size)`.
|
| 76 |
+
audio_hidden_states of the model produced by the speech encoder and after projecting the last hidden state.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
audio_hidden_states: torch.FloatTensor | None = None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@auto_docstring(
|
| 83 |
+
custom_intro="""
|
| 84 |
+
Base class for Lfm3Omni causal language model (or autoregressive) outputs.
|
| 85 |
+
"""
|
| 86 |
+
)
|
| 87 |
+
@dataclass
|
| 88 |
+
class Lfm3OmniCausalLMOutputWithPast(CausalLMOutputWithPast):
|
| 89 |
+
r"""
|
| 90 |
+
audio_hidden_states (`torch.FloatTensor`, *optional*):
|
| 91 |
+
A `torch.FloatTensor` of size `(num_audio_tokens, hidden_size)`.
|
| 92 |
+
audio_hidden_states of the model produced by the speech encoder and after projecting the last hidden state.
|
| 93 |
+
text_loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided):
|
| 94 |
+
The language modeling loss of the text tokens.
|
| 95 |
+
audio_loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `audio_labels` is provided):
|
| 96 |
+
The depth decoder loss of the speech frames.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
audio_hidden_states: torch.FloatTensor | None = None
|
| 100 |
+
text_loss: torch.FloatTensor | None = None
|
| 101 |
+
audio_loss: torch.FloatTensor | None = None
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class Lfm3OmniEncoderStreamingCache:
|
| 105 |
+
"""Bounded downsampler context and causal encoder state for synchronized audio chunks."""
|
| 106 |
+
|
| 107 |
+
def __init__(self, config: Lfm3OmniEncoderConfig):
|
| 108 |
+
cache_config = copy.deepcopy(config.text_config)
|
| 109 |
+
cache_config.sliding_window = config.sliding_window
|
| 110 |
+
cache_config.layer_types = [
|
| 111 |
+
"sliding_attention" if kind == "full_attention" else kind for kind in cache_config.layer_types
|
| 112 |
+
]
|
| 113 |
+
self.past_key_values = DynamicCache(config=cache_config)
|
| 114 |
+
self.features: torch.Tensor | None = None
|
| 115 |
+
self.offset = 0
|
| 116 |
+
self.num_frames = 0
|
| 117 |
+
self.num_tokens = 0
|
| 118 |
+
self.finished = False
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@auto_docstring(
|
| 122 |
+
custom_intro="""
|
| 123 |
+
Outputs of [`Lfm3OmniForConditionalGeneration.generate`].
|
| 124 |
+
"""
|
| 125 |
+
)
|
| 126 |
+
@dataclass
|
| 127 |
+
class Lfm3OmniGenerateOutput(ModelOutput):
|
| 128 |
+
r"""
|
| 129 |
+
sequences (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
|
| 130 |
+
The prompt followed by the generated tokens: text tokens as their ids, and one `audio_out_token_id` per
|
| 131 |
+
generated speech frame.
|
| 132 |
+
audio_codes (`torch.LongTensor` of shape `(batch_size, num_codebooks, num_frames)`):
|
| 133 |
+
The codes of the generated speech frames in generation order, without the end-of-audio frames unless
|
| 134 |
+
`return_audio_eos` was set. Rows are padded with -1; slice to `audio_lengths` before codec decoding.
|
| 135 |
+
sequence_lengths (`torch.LongTensor` of shape `(batch_size,)`):
|
| 136 |
+
The prompt width (including input padding) plus the number of returned generated slots for each row.
|
| 137 |
+
audio_lengths (`torch.LongTensor` of shape `(batch_size,)`):
|
| 138 |
+
The number of returned speech frames per row.
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
sequences: torch.LongTensor = None
|
| 142 |
+
audio_codes: torch.LongTensor | None = None
|
| 143 |
+
sequence_lengths: torch.LongTensor | None = None
|
| 144 |
+
audio_lengths: torch.LongTensor | None = None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@auto_docstring(custom_intro="State returned by [`Lfm3OmniForConditionalGeneration.prefill_audio`].")
|
| 148 |
+
@dataclass
|
| 149 |
+
class Lfm3OmniStreamingOutput(ModelOutput):
|
| 150 |
+
r"""
|
| 151 |
+
past_key_values (`Cache`, *optional*):
|
| 152 |
+
Language-model cache including all completed audio tokens. Updated in place by subsequent calls.
|
| 153 |
+
encoder_cache (`Lfm3OmniEncoderStreamingCache`, *optional*):
|
| 154 |
+
Streaming encoder state for the current utterance. Start a new encoder cache for each utterance.
|
| 155 |
+
attention_mask (`torch.LongTensor` of shape `(batch_size, cached_sequence_length)`, *optional*):
|
| 156 |
+
Full language-model mask, including the text prefix and completed audio tokens.
|
| 157 |
+
last_hidden_state (`torch.FloatTensor` of shape `(batch_size, num_new_tokens, hidden_size)`, *optional*):
|
| 158 |
+
Language-model states for this chunk, or `None` when no audio token was completed.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
past_key_values: Cache | None = None
|
| 162 |
+
encoder_cache: Lfm3OmniEncoderStreamingCache | None = None
|
| 163 |
+
attention_mask: torch.LongTensor | None = None
|
| 164 |
+
last_hidden_state: torch.FloatTensor | None = None
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class Lfm3OmniLiveTranscriptionState:
|
| 168 |
+
"""State of one live transcription: the encoder follows the whole stream, the language model one context."""
|
| 169 |
+
|
| 170 |
+
def __init__(self):
|
| 171 |
+
self.encoder_cache: Lfm3OmniEncoderStreamingCache | None = None
|
| 172 |
+
self.past_key_values: Cache | None = None
|
| 173 |
+
self.attention_mask: torch.LongTensor | None = None
|
| 174 |
+
# fed before the next chunk's audio: the prompt of a new context, or the `<|text_end|>` closing the last block
|
| 175 |
+
self.pending: torch.FloatTensor | None = None
|
| 176 |
+
self.finished = False
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@auto_docstring(custom_intro="Output of [`Lfm3OmniForConditionalGeneration.transcribe_live`].")
|
| 180 |
+
@dataclass
|
| 181 |
+
class Lfm3OmniLiveTranscriptionOutput(ModelOutput):
|
| 182 |
+
r"""
|
| 183 |
+
sequences (`torch.LongTensor` of shape `(1, num_generated)`):
|
| 184 |
+
The text tokens of this chunk, without the `<|text_end|>` closing the block or the final stop token.
|
| 185 |
+
num_audio_tokens (`int`):
|
| 186 |
+
Audio tokens the chunk completed.
|
| 187 |
+
closed (`bool`):
|
| 188 |
+
Whether the model ended the block itself, with `<|text_end|>` or a stop token on the final chunk, instead of
|
| 189 |
+
exhausting `max_new_tokens`.
|
| 190 |
+
state ([`Lfm3OmniLiveTranscriptionState`]):
|
| 191 |
+
The stream state for the next call.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
sequences: torch.LongTensor | None = None
|
| 195 |
+
num_audio_tokens: int | None = None
|
| 196 |
+
closed: bool | None = None
|
| 197 |
+
state: Lfm3OmniLiveTranscriptionState | None = None
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class Lfm3OmniRMSNorm(nn.Module):
|
| 201 |
+
def __init__(self, hidden_size, eps: float = 1e-6) -> None:
|
| 202 |
+
super().__init__()
|
| 203 |
+
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 204 |
+
self.variance_epsilon = eps
|
| 205 |
+
|
| 206 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 207 |
+
# The trained heads scale in fp32 and round once (liquid_lfm `RMSNorm`), not weight * bf16(x) like the backbone.
|
| 208 |
+
input_dtype = hidden_states.dtype
|
| 209 |
+
hidden_states = hidden_states.to(torch.float32)
|
| 210 |
+
variance = hidden_states.pow(2).mean(-1, keepdim=True)
|
| 211 |
+
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
|
| 212 |
+
return (hidden_states * self.weight).to(input_dtype)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class Lfm3OmniCodebookEmbedding(nn.Module):
|
| 216 |
+
"""Input embedding and tied output head of one codebook of the depth decoder."""
|
| 217 |
+
|
| 218 |
+
def __init__(self, config: Lfm3OmniConfig):
|
| 219 |
+
super().__init__()
|
| 220 |
+
self.embedding = nn.Embedding(config.codebook_size, config.depth_config.hidden_size)
|
| 221 |
+
self.embedding_norm = Lfm3OmniRMSNorm(config.depth_config.hidden_size, eps=config.depth_config.norm_eps)
|
| 222 |
+
|
| 223 |
+
def forward(self, codes: torch.Tensor) -> torch.Tensor:
|
| 224 |
+
return self.embedding(codes)
|
| 225 |
+
|
| 226 |
+
def logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 227 |
+
return F.linear(self.embedding_norm(hidden_states), self.embedding.weight)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
class Lfm3OmniDepthDecoder(nn.Module):
|
| 231 |
+
"""
|
| 232 |
+
Predicts the codebooks of one speech frame from the language model's hidden state. Every codebook gets its own
|
| 233 |
+
slot of the projected hidden state plus either its known code embedding or a learned mask embedding; a
|
| 234 |
+
bidirectional LFM3 model over the slots then scores the masked codebooks.
|
| 235 |
+
"""
|
| 236 |
+
|
| 237 |
+
def __init__(self, config: Lfm3OmniConfig):
|
| 238 |
+
super().__init__()
|
| 239 |
+
self.config = config
|
| 240 |
+
depth_config = config.depth_config
|
| 241 |
+
self.num_codebooks = config.num_codebooks
|
| 242 |
+
self.hidden_size = depth_config.hidden_size
|
| 243 |
+
self.depth_linear = nn.Linear(config.text_config.hidden_size, self.num_codebooks * self.hidden_size)
|
| 244 |
+
self.mask_embedding = nn.Parameter(torch.empty(self.hidden_size))
|
| 245 |
+
self.model = Lfm3Model(depth_config)
|
| 246 |
+
self.model.register_module("embed_tokens", None)
|
| 247 |
+
self.depth_embeddings = nn.ModuleList(Lfm3OmniCodebookEmbedding(config) for _ in range(self.num_codebooks))
|
| 248 |
+
|
| 249 |
+
def embed_codes(self, codes: torch.Tensor) -> torch.Tensor:
|
| 250 |
+
return torch.stack(
|
| 251 |
+
[embedding(codes[..., index]) for index, embedding in enumerate(self.depth_embeddings)], dim=-2
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
def forward(self, hidden_states: torch.Tensor, codes: torch.Tensor, masked: torch.Tensor) -> torch.Tensor:
|
| 255 |
+
r"""
|
| 256 |
+
hidden_states (`torch.FloatTensor` of shape `(num_frames, hidden_size)`):
|
| 257 |
+
The language model's hidden state at the position preceding each frame.
|
| 258 |
+
codes (`torch.LongTensor` of shape `(num_frames, num_codebooks)`):
|
| 259 |
+
The codes of each frame.
|
| 260 |
+
masked (`torch.BoolTensor` of shape `(num_frames, num_codebooks)`):
|
| 261 |
+
The codebooks hidden from the decoder, which it is asked to predict.
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
`torch.FloatTensor` of shape `(num_frames, num_codebooks, codebook_size)`: the logits of every codebook.
|
| 265 |
+
"""
|
| 266 |
+
depth_in = self.depth_linear(hidden_states).unflatten(-1, (self.num_codebooks, self.hidden_size))
|
| 267 |
+
code_embeds = self.embed_codes(codes)
|
| 268 |
+
depth_in = depth_in + torch.where(masked[..., None], self.mask_embedding.to(code_embeds.dtype), code_embeds)
|
| 269 |
+
depth_out = self.trunk(depth_in)
|
| 270 |
+
return torch.stack(
|
| 271 |
+
[embedding.logits(depth_out[:, index]) for index, embedding in enumerate(self.depth_embeddings)], dim=1
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
def trunk(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 275 |
+
# all codebooks of a frame are predicted jointly, so the attention is bidirectional
|
| 276 |
+
return self.model(
|
| 277 |
+
inputs_embeds=hidden_states,
|
| 278 |
+
attention_mask={"full_attention": None},
|
| 279 |
+
is_causal=False,
|
| 280 |
+
use_cache=False,
|
| 281 |
+
).last_hidden_state
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def create_masks_by_layer_type(
|
| 285 |
+
config: PreTrainedConfig,
|
| 286 |
+
inputs_embeds: torch.Tensor,
|
| 287 |
+
attention_mask: torch.Tensor | None,
|
| 288 |
+
past_key_values: Cache | None,
|
| 289 |
+
position_ids: torch.Tensor | None = None,
|
| 290 |
+
) -> dict:
|
| 291 |
+
"""The cache sizes each mask from the layer it is asked about, and the generic mask utilities ask about the first
|
| 292 |
+
non-sliding layer, which for a leading `conv` layer redirects to the first attention layer of any type: name the
|
| 293 |
+
first layer of each mask's own type instead."""
|
| 294 |
+
config = config.get_text_config()
|
| 295 |
+
mask_kwargs = {
|
| 296 |
+
"config": config,
|
| 297 |
+
"inputs_embeds": inputs_embeds,
|
| 298 |
+
"attention_mask": attention_mask,
|
| 299 |
+
"past_key_values": past_key_values,
|
| 300 |
+
"position_ids": position_ids,
|
| 301 |
+
}
|
| 302 |
+
layer_types, causal_mask_mapping = config.layer_types, {}
|
| 303 |
+
if "full_attention" in layer_types:
|
| 304 |
+
layer_idx = layer_types.index("full_attention")
|
| 305 |
+
causal_mask_mapping["full_attention"] = create_causal_mask(**mask_kwargs, layer_idx=layer_idx)
|
| 306 |
+
if "sliding_attention" in layer_types:
|
| 307 |
+
layer_idx = layer_types.index("sliding_attention")
|
| 308 |
+
causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(
|
| 309 |
+
**mask_kwargs, layer_idx=layer_idx
|
| 310 |
+
)
|
| 311 |
+
if "conv" in layer_types:
|
| 312 |
+
causal_mask_mapping["conv"] = create_recurrent_attention_mask(**mask_kwargs)
|
| 313 |
+
return causal_mask_mapping
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
@auto_docstring
|
| 317 |
+
class Lfm3OmniPreTrainedModel(PreTrainedModel):
|
| 318 |
+
config: Lfm3OmniConfig
|
| 319 |
+
base_model_prefix = "model"
|
| 320 |
+
supports_gradient_checkpointing = True
|
| 321 |
+
_no_split_modules = ["Lfm3OmniDepthDecoder"]
|
| 322 |
+
_skip_keys_device_placement = ["past_key_values"]
|
| 323 |
+
_supports_flash_attn = True
|
| 324 |
+
_supports_sdpa = True
|
| 325 |
+
_supports_flex_attn = True
|
| 326 |
+
|
| 327 |
+
_can_compile_fullgraph = True
|
| 328 |
+
_supports_attention_backend = True
|
| 329 |
+
input_modalities = ("image", "video", "audio", "text")
|
| 330 |
+
|
| 331 |
+
@torch.no_grad()
|
| 332 |
+
def _init_weights(self, module):
|
| 333 |
+
super()._init_weights(module)
|
| 334 |
+
if isinstance(module, Lfm3OmniDepthDecoder):
|
| 335 |
+
init.normal_(module.mask_embedding, std=0.02)
|
| 336 |
+
|
| 337 |
+
@staticmethod
|
| 338 |
+
def create_masks_for_generate(
|
| 339 |
+
config: PreTrainedConfig,
|
| 340 |
+
inputs_embeds: torch.Tensor,
|
| 341 |
+
attention_mask: torch.Tensor | None,
|
| 342 |
+
past_key_values: Cache | None,
|
| 343 |
+
position_ids: torch.Tensor | None = None,
|
| 344 |
+
**kwargs,
|
| 345 |
+
) -> dict:
|
| 346 |
+
return create_masks_by_layer_type(config, inputs_embeds, attention_mask, past_key_values, position_ids)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class Lfm3OmniMultiModalProjector(nn.Module):
|
| 350 |
+
"""Merges each block of `downsample_factor**2` vision tokens into one language token."""
|
| 351 |
+
|
| 352 |
+
def __init__(self, config: Lfm3OmniConfig):
|
| 353 |
+
super().__init__()
|
| 354 |
+
self.downsample_factor = config.downsample_factor
|
| 355 |
+
self.layer_norm = nn.LayerNorm(config.vision_config.hidden_size, eps=config.projector_layer_norm_eps)
|
| 356 |
+
self.linear_1 = nn.Linear(
|
| 357 |
+
config.vision_config.hidden_size * config.downsample_factor**2,
|
| 358 |
+
config.projector_hidden_size,
|
| 359 |
+
bias=config.projector_bias,
|
| 360 |
+
)
|
| 361 |
+
self.act = ACT2FN[config.projector_hidden_act]
|
| 362 |
+
self.linear_2 = nn.Linear(
|
| 363 |
+
config.projector_hidden_size, config.text_config.hidden_size, bias=config.projector_bias
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
def forward(self, image_features: torch.Tensor):
|
| 367 |
+
hidden_states = self.layer_norm(image_features).reshape(-1, self.linear_1.in_features)
|
| 368 |
+
hidden_states = self.linear_1(hidden_states)
|
| 369 |
+
hidden_states = self.act(hidden_states)
|
| 370 |
+
hidden_states = self.linear_2(hidden_states)
|
| 371 |
+
return hidden_states
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
class Lfm3OmniDownsampler(nn.Module):
|
| 375 |
+
"""Three stride-2 convolutions over the (mel, time) plane: 128 mel bins and 8 frames become one vector."""
|
| 376 |
+
|
| 377 |
+
def __init__(self, config: Lfm3OmniEncoderConfig):
|
| 378 |
+
super().__init__()
|
| 379 |
+
channels = config.conv_channels
|
| 380 |
+
conv = {"kernel_size": 3, "stride": 2, "padding": 1}
|
| 381 |
+
self.conv_in = nn.Conv2d(1, channels, **conv)
|
| 382 |
+
self.conv_mid = nn.Conv2d(channels, channels, groups=channels, **conv)
|
| 383 |
+
self.lin_mid = nn.Conv2d(channels, channels, kernel_size=1)
|
| 384 |
+
self.conv_end = nn.Conv2d(channels, channels, groups=channels, **conv)
|
| 385 |
+
self.lin_end = nn.Conv2d(channels, channels, kernel_size=1)
|
| 386 |
+
self.lin_out = nn.Conv1d(config.num_mel_bins * channels // 8, config.hidden_size, kernel_size=1)
|
| 387 |
+
self.act = nn.SiLU()
|
| 388 |
+
|
| 389 |
+
def forward(self, input_features: torch.Tensor, feature_lengths: torch.Tensor | None = None) -> torch.Tensor:
|
| 390 |
+
def mask_padding(hidden_states, lengths):
|
| 391 |
+
if lengths is None:
|
| 392 |
+
return hidden_states
|
| 393 |
+
valid = torch.arange(hidden_states.shape[-1], device=hidden_states.device)[None] < lengths[:, None]
|
| 394 |
+
return hidden_states * valid[:, None, None].to(hidden_states.dtype)
|
| 395 |
+
|
| 396 |
+
hidden_states = mask_padding(input_features.unsqueeze(1), feature_lengths)
|
| 397 |
+
hidden_states = self.act(self.conv_in(hidden_states))
|
| 398 |
+
lengths = None if feature_lengths is None else (feature_lengths + 1) // 2
|
| 399 |
+
hidden_states = mask_padding(hidden_states, lengths)
|
| 400 |
+
hidden_states = self.act(self.lin_mid(self.conv_mid(hidden_states)))
|
| 401 |
+
lengths = None if lengths is None else (lengths + 1) // 2
|
| 402 |
+
hidden_states = mask_padding(hidden_states, lengths)
|
| 403 |
+
hidden_states = self.act(self.lin_end(self.conv_end(hidden_states)))
|
| 404 |
+
return self.lin_out(hidden_states.flatten(1, 2))
|
| 405 |
+
|
| 406 |
+
@staticmethod
|
| 407 |
+
def output_lengths(lengths: torch.Tensor) -> torch.Tensor:
|
| 408 |
+
return -(lengths // -8)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class Lfm3OmniEncoder(nn.Module):
|
| 412 |
+
def __init__(self, config: Lfm3OmniEncoderConfig):
|
| 413 |
+
super().__init__()
|
| 414 |
+
self.config = config
|
| 415 |
+
self.downsample = Lfm3OmniDownsampler(config)
|
| 416 |
+
self.lfm = AutoModel.from_config(config.text_config)
|
| 417 |
+
# the encoder only ever consumes spectrogram features
|
| 418 |
+
self.lfm.register_module("embed_tokens", None)
|
| 419 |
+
|
| 420 |
+
def forward(
|
| 421 |
+
self, input_features: torch.Tensor, feature_lengths: torch.Tensor
|
| 422 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 423 |
+
hidden_states = self.downsample(input_features, feature_lengths).transpose(1, 2)
|
| 424 |
+
# The mask bounds the window for mask-based backends; flash attention uses the kernel kwarg.
|
| 425 |
+
self.lfm.config.sliding_window = self.config.sliding_window
|
| 426 |
+
attention_mask = create_sliding_window_causal_mask(
|
| 427 |
+
config=self.lfm.config,
|
| 428 |
+
inputs_embeds=hidden_states,
|
| 429 |
+
attention_mask=None,
|
| 430 |
+
past_key_values=None,
|
| 431 |
+
position_ids=None,
|
| 432 |
+
)
|
| 433 |
+
outputs = self.lfm(
|
| 434 |
+
inputs_embeds=hidden_states,
|
| 435 |
+
attention_mask={"full_attention": attention_mask, "conv": None},
|
| 436 |
+
use_cache=False,
|
| 437 |
+
sliding_window=self.config.sliding_window,
|
| 438 |
+
)
|
| 439 |
+
return outputs.last_hidden_state, self.downsample.output_lengths(feature_lengths)
|
| 440 |
+
|
| 441 |
+
@torch.no_grad()
|
| 442 |
+
def stream(
|
| 443 |
+
self,
|
| 444 |
+
input_features: torch.Tensor,
|
| 445 |
+
cache: Lfm3OmniEncoderStreamingCache | None = None,
|
| 446 |
+
is_final: bool = False,
|
| 447 |
+
) -> tuple[torch.Tensor, Lfm3OmniEncoderStreamingCache]:
|
| 448 |
+
"""Encode complete stride-8 windows, with seven frames of lookahead and bounded left context."""
|
| 449 |
+
if self.training:
|
| 450 |
+
raise ValueError("Streaming audio encoding requires eval mode")
|
| 451 |
+
if cache is None:
|
| 452 |
+
cache = Lfm3OmniEncoderStreamingCache(self.config)
|
| 453 |
+
if cache.finished:
|
| 454 |
+
raise ValueError("This stream is finalized; start with a new cache")
|
| 455 |
+
features = input_features.to(self.downsample.conv_in.weight.dtype)
|
| 456 |
+
if features.ndim != 3 or features.shape[1] != self.config.num_mel_bins:
|
| 457 |
+
raise ValueError("Expected features of shape (batch_size, num_mel_bins, num_frames)")
|
| 458 |
+
if cache.features is not None:
|
| 459 |
+
features = torch.cat((cache.features, features), dim=-1)
|
| 460 |
+
num_frames = cache.num_frames + input_features.shape[-1]
|
| 461 |
+
num_tokens = (num_frames + (7 if is_final else 0)) // 8
|
| 462 |
+
count = num_tokens - cache.num_tokens
|
| 463 |
+
if count:
|
| 464 |
+
start = cache.num_tokens - cache.offset // 8
|
| 465 |
+
hidden_states = self.downsample(features).transpose(1, 2)[:, start : start + count]
|
| 466 |
+
positions = torch.arange(cache.num_tokens, num_tokens, device=features.device)[None]
|
| 467 |
+
self.lfm.config.sliding_window = self.config.sliding_window
|
| 468 |
+
mask = create_sliding_window_causal_mask(
|
| 469 |
+
config=self.lfm.config,
|
| 470 |
+
inputs_embeds=hidden_states,
|
| 471 |
+
attention_mask=None,
|
| 472 |
+
past_key_values=cache.past_key_values,
|
| 473 |
+
position_ids=positions,
|
| 474 |
+
layer_idx=self.lfm.config.layer_types.index("full_attention"),
|
| 475 |
+
)
|
| 476 |
+
try:
|
| 477 |
+
outputs = self.lfm(
|
| 478 |
+
inputs_embeds=hidden_states,
|
| 479 |
+
attention_mask={"full_attention": mask, "conv": None},
|
| 480 |
+
past_key_values=cache.past_key_values,
|
| 481 |
+
position_ids=positions,
|
| 482 |
+
use_cache=True,
|
| 483 |
+
sliding_window=self.config.sliding_window,
|
| 484 |
+
)
|
| 485 |
+
except Exception:
|
| 486 |
+
# the layers already run have advanced the key/value cache: the stream cannot be resumed
|
| 487 |
+
cache.finished = True
|
| 488 |
+
raise
|
| 489 |
+
encoded = outputs.last_hidden_state
|
| 490 |
+
else:
|
| 491 |
+
encoded = features.new_empty((features.shape[0], 0, self.config.hidden_size))
|
| 492 |
+
offset = max(0, (num_tokens - 1) * 8)
|
| 493 |
+
cache.features = features[..., offset - cache.offset :].clone() if not is_final else None
|
| 494 |
+
cache.num_frames, cache.offset, cache.num_tokens, cache.finished = num_frames, offset, num_tokens, is_final
|
| 495 |
+
return encoded, cache
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
class Lfm3OmniProjector(nn.Module):
|
| 499 |
+
def __init__(self, config: Lfm3OmniConfig):
|
| 500 |
+
super().__init__()
|
| 501 |
+
in_features = config.audio_encoder_config.hidden_size
|
| 502 |
+
out_features = config.text_config.hidden_size
|
| 503 |
+
self.layer_norm = nn.LayerNorm(in_features)
|
| 504 |
+
self.linear_1 = nn.Linear(in_features, out_features)
|
| 505 |
+
self.act = nn.GELU()
|
| 506 |
+
self.linear_2 = nn.Linear(out_features, out_features)
|
| 507 |
+
|
| 508 |
+
def forward(self, audio_features: torch.Tensor) -> torch.Tensor:
|
| 509 |
+
return self.linear_2(self.act(self.linear_1(self.layer_norm(audio_features))))
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
@auto_docstring
|
| 513 |
+
class Lfm3OmniModel(Lfm3OmniPreTrainedModel):
|
| 514 |
+
def __init__(self, config: Lfm3OmniConfig):
|
| 515 |
+
super().__init__(config)
|
| 516 |
+
self.language_model = Lfm3Model(config.text_config)
|
| 517 |
+
self.audio_encoder = Lfm3OmniEncoder(config.audio_encoder_config)
|
| 518 |
+
self.audio_projector = Lfm3OmniProjector(config)
|
| 519 |
+
self.codebook_embedding = nn.Embedding(
|
| 520 |
+
config.num_codebooks * config.codebook_size, config.text_config.hidden_size
|
| 521 |
+
)
|
| 522 |
+
self.depth_decoder = Lfm3OmniDepthDecoder(config)
|
| 523 |
+
self.vision_tower = Lfm3VlVisionModel(config.vision_config)
|
| 524 |
+
self.multi_modal_projector = Lfm3OmniMultiModalProjector(config)
|
| 525 |
+
self.post_init()
|
| 526 |
+
|
| 527 |
+
def get_input_embeddings(self):
|
| 528 |
+
return self.language_model.get_input_embeddings()
|
| 529 |
+
|
| 530 |
+
def set_input_embeddings(self, value):
|
| 531 |
+
self.language_model.set_input_embeddings(value)
|
| 532 |
+
|
| 533 |
+
@auto_docstring
|
| 534 |
+
def get_audio_features(
|
| 535 |
+
self, input_features: torch.FloatTensor, feature_attention_mask: torch.Tensor
|
| 536 |
+
) -> torch.FloatTensor:
|
| 537 |
+
r"""
|
| 538 |
+
input_features (`torch.FloatTensor` of shape `(num_clips, num_mel_bins, num_frames)`):
|
| 539 |
+
The log-mel spectrograms of the input clips, right-padded to the longest clip.
|
| 540 |
+
feature_attention_mask (`torch.Tensor` of shape `(num_clips, num_frames)`):
|
| 541 |
+
The mask of the valid spectrogram frames of each clip.
|
| 542 |
+
|
| 543 |
+
Returns:
|
| 544 |
+
`torch.FloatTensor` of shape `(num_audio_tokens, hidden_size)`: the projected speech features of all clips
|
| 545 |
+
in order, one per eight spectrogram frames.
|
| 546 |
+
"""
|
| 547 |
+
# the streaming paths refuse these shapes too; offline, a few missing mel bins would encode silently
|
| 548 |
+
num_mel_bins = self.config.audio_encoder_config.num_mel_bins
|
| 549 |
+
if input_features.ndim != 3 or input_features.shape[1] != num_mel_bins:
|
| 550 |
+
raise ValueError(
|
| 551 |
+
f"Expected input_features of shape (num_clips, {num_mel_bins}, num_frames), got "
|
| 552 |
+
f"{tuple(input_features.shape)}"
|
| 553 |
+
)
|
| 554 |
+
if feature_attention_mask.shape != (input_features.shape[0], input_features.shape[-1]):
|
| 555 |
+
raise ValueError(
|
| 556 |
+
f"Expected feature_attention_mask of shape {(input_features.shape[0], input_features.shape[-1])}, "
|
| 557 |
+
f"got {tuple(feature_attention_mask.shape)}"
|
| 558 |
+
)
|
| 559 |
+
feature_lengths = feature_attention_mask.sum(-1)
|
| 560 |
+
input_features = input_features.to(self.audio_encoder.downsample.conv_in.weight.dtype)
|
| 561 |
+
encoded, encoded_lengths = self.audio_encoder(input_features, feature_lengths)
|
| 562 |
+
valid = torch.arange(encoded.shape[1], device=encoded.device)[None, :] < encoded_lengths[:, None]
|
| 563 |
+
return self.audio_projector(encoded[valid])
|
| 564 |
+
|
| 565 |
+
@torch.no_grad()
|
| 566 |
+
def get_audio_features_stream(
|
| 567 |
+
self,
|
| 568 |
+
input_features: torch.FloatTensor,
|
| 569 |
+
cache: Lfm3OmniEncoderStreamingCache | None = None,
|
| 570 |
+
is_final: bool = False,
|
| 571 |
+
) -> tuple[torch.Tensor, Lfm3OmniEncoderStreamingCache]:
|
| 572 |
+
"""Project newly completed audio tokens, returning `(batch, tokens, hidden)` and the updated cache."""
|
| 573 |
+
encoded, cache = self.audio_encoder.stream(input_features, cache, is_final)
|
| 574 |
+
return self.audio_projector(encoded), cache
|
| 575 |
+
|
| 576 |
+
def get_audio_out_embeds(self, audio_out_codes: torch.LongTensor) -> torch.FloatTensor:
|
| 577 |
+
r"""
|
| 578 |
+
audio_out_codes (`torch.LongTensor` of shape `(num_frames, num_codebooks)`):
|
| 579 |
+
The codes of the speech frames in the prompt.
|
| 580 |
+
|
| 581 |
+
Returns:
|
| 582 |
+
`torch.FloatTensor` of shape `(num_frames, hidden_size)`: the sum of the codebook embeddings of each frame.
|
| 583 |
+
"""
|
| 584 |
+
num_codebooks, codebook_size = self.config.num_codebooks, self.config.codebook_size
|
| 585 |
+
if audio_out_codes.ndim != 2 or audio_out_codes.shape[-1] != num_codebooks:
|
| 586 |
+
raise ValueError(
|
| 587 |
+
f"Expected audio_out_codes of shape (num_frames, {num_codebooks}), got {tuple(audio_out_codes.shape)}"
|
| 588 |
+
)
|
| 589 |
+
# the codebooks share one table: an out-of-range code would silently read a neighbouring codebook. The check
|
| 590 |
+
# is data-dependent, so a traced or captured forward skips it (`generate` prefills eagerly)
|
| 591 |
+
if not is_tracing(audio_out_codes):
|
| 592 |
+
torch_compilable_check(
|
| 593 |
+
(audio_out_codes >= 0) & (audio_out_codes < codebook_size),
|
| 594 |
+
f"audio_out_codes must be in [0, {codebook_size})",
|
| 595 |
+
)
|
| 596 |
+
offsets = torch.arange(num_codebooks, device=audio_out_codes.device) * codebook_size
|
| 597 |
+
return self.codebook_embedding(audio_out_codes + offsets).sum(-2)
|
| 598 |
+
|
| 599 |
+
def get_placeholder_positions(self, input_ids, inputs_embeds, token_id: int) -> torch.Tensor:
|
| 600 |
+
if input_ids is not None:
|
| 601 |
+
return input_ids == token_id
|
| 602 |
+
token_embed = self.get_input_embeddings()(
|
| 603 |
+
torch.full((), token_id, dtype=torch.long, device=inputs_embeds.device)
|
| 604 |
+
)
|
| 605 |
+
return (inputs_embeds == token_embed).all(-1)
|
| 606 |
+
|
| 607 |
+
def check_speech_placeholders(
|
| 608 |
+
self, input_ids, inputs_embeds, input_features=None, audio_out_codes=None, live: bool = False
|
| 609 |
+
) -> None:
|
| 610 |
+
"""A speech placeholder with nothing behind it would be read as text and switch `generate` into speech mode.
|
| 611 |
+
The check is data-dependent, so a compiled, exported or CUDA-graph-captured forward skips it; `generate` and
|
| 612 |
+
`transcribe_live` run eagerly."""
|
| 613 |
+
if is_tracing(input_ids if input_ids is not None else inputs_embeds):
|
| 614 |
+
return
|
| 615 |
+
for field, supplied, name in (
|
| 616 |
+
("audio_token_id", input_features is not None, "input_features"),
|
| 617 |
+
("audio_out_token_id", audio_out_codes is not None, "audio_out_codes"),
|
| 618 |
+
):
|
| 619 |
+
if not supplied:
|
| 620 |
+
torch_compilable_check(
|
| 621 |
+
~self.get_placeholder_positions(input_ids, inputs_embeds, getattr(self.config, field)),
|
| 622 |
+
"Live transcription prompts cannot hold speech placeholders: the audio arrives as `input_features`"
|
| 623 |
+
if live
|
| 624 |
+
else f"{field} placeholders require `{name}`",
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
def get_placeholder_mask(self, input_ids, inputs_embeds, token_id: int, num_features: int) -> torch.Tensor:
|
| 628 |
+
mask = self.get_placeholder_positions(input_ids, inputs_embeds, token_id)
|
| 629 |
+
# data-dependent, so a traced or captured forward skips it, like `check_speech_placeholders`
|
| 630 |
+
if not is_tracing(mask):
|
| 631 |
+
torch_compilable_check(
|
| 632 |
+
mask.sum() == num_features,
|
| 633 |
+
lambda: f"Placeholder tokens and features do not match: {int(mask.sum())} tokens {token_id}, "
|
| 634 |
+
f"{num_features} features",
|
| 635 |
+
)
|
| 636 |
+
return mask.unsqueeze(-1).to(inputs_embeds.device)
|
| 637 |
+
|
| 638 |
+
@can_return_tuple
|
| 639 |
+
@auto_docstring
|
| 640 |
+
def forward(
|
| 641 |
+
self,
|
| 642 |
+
input_ids: torch.LongTensor | None = None,
|
| 643 |
+
attention_mask: torch.Tensor | None = None,
|
| 644 |
+
position_ids: torch.LongTensor | None = None,
|
| 645 |
+
past_key_values: Cache | None = None,
|
| 646 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 647 |
+
input_features: torch.FloatTensor | None = None,
|
| 648 |
+
feature_attention_mask: torch.Tensor | None = None,
|
| 649 |
+
audio_out_codes: torch.LongTensor | None = None,
|
| 650 |
+
pixel_values: torch.FloatTensor | None = None,
|
| 651 |
+
image_grid_thw: torch.LongTensor | None = None,
|
| 652 |
+
pixel_values_videos: torch.FloatTensor | None = None,
|
| 653 |
+
video_grid_thw: torch.LongTensor | None = None,
|
| 654 |
+
use_cache: bool | None = None,
|
| 655 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 656 |
+
) -> tuple | Lfm3OmniModelOutputWithPast:
|
| 657 |
+
r"""
|
| 658 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 659 |
+
Flattened image patches. `patch_dim` is channels times temporal patch size times squared spatial patch size.
|
| 660 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 661 |
+
Temporal, height, and width patch counts before spatial merging, in image placeholder order.
|
| 662 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 663 |
+
Flattened video patches; the tower encodes every temporal patch as its own spatial sequence.
|
| 664 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
|
| 665 |
+
Temporal, height, and width patch counts before spatial merging, in video placeholder order.
|
| 666 |
+
input_features (`torch.FloatTensor` of shape `(num_clips, num_mel_bins, num_frames)`, *optional*):
|
| 667 |
+
The log-mel spectrograms of the input clips, right-padded to the longest clip.
|
| 668 |
+
feature_attention_mask (`torch.Tensor` of shape `(num_clips, num_frames)`, *optional*):
|
| 669 |
+
The mask of the valid spectrogram frames of each clip.
|
| 670 |
+
audio_out_codes (`torch.LongTensor` of shape `(num_frames, num_codebooks)`, *optional*):
|
| 671 |
+
The codes of the speech frames in the prompt, one row per `audio_out_token_id` placeholder. A speech
|
| 672 |
+
placeholder without its features or codes is refused, except in a compiled or captured forward, which
|
| 673 |
+
skips the check.
|
| 674 |
+
"""
|
| 675 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 676 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 677 |
+
if inputs_embeds is None:
|
| 678 |
+
inputs_embeds = self.get_input_embeddings()(input_ids)
|
| 679 |
+
# vision placeholders follow the Qwen3-VL convention and are left to the count check
|
| 680 |
+
self.check_speech_placeholders(input_ids, inputs_embeds, input_features, audio_out_codes)
|
| 681 |
+
|
| 682 |
+
if (pixel_values is None) != (image_grid_thw is None):
|
| 683 |
+
raise ValueError("pixel_values and image_grid_thw must be supplied together")
|
| 684 |
+
if pixel_values is not None:
|
| 685 |
+
features = self.get_image_features(pixel_values, image_grid_thw).to(inputs_embeds)
|
| 686 |
+
mask = self.get_placeholder_mask(input_ids, inputs_embeds, self.config.image_token_id, features.shape[0])
|
| 687 |
+
inputs_embeds = inputs_embeds.masked_scatter(mask, features)
|
| 688 |
+
if (pixel_values_videos is None) != (video_grid_thw is None):
|
| 689 |
+
raise ValueError("pixel_values_videos and video_grid_thw must be supplied together")
|
| 690 |
+
if pixel_values_videos is not None:
|
| 691 |
+
features = self.get_video_features(pixel_values_videos, video_grid_thw).to(inputs_embeds)
|
| 692 |
+
mask = self.get_placeholder_mask(input_ids, inputs_embeds, self.config.video_token_id, features.shape[0])
|
| 693 |
+
inputs_embeds = inputs_embeds.masked_scatter(mask, features)
|
| 694 |
+
|
| 695 |
+
audio_features = None
|
| 696 |
+
if input_features is not None:
|
| 697 |
+
if feature_attention_mask is None:
|
| 698 |
+
feature_attention_mask = torch.ones(
|
| 699 |
+
input_features.shape[0], input_features.shape[-1], dtype=torch.bool, device=input_features.device
|
| 700 |
+
)
|
| 701 |
+
audio_features = self.get_audio_features(input_features, feature_attention_mask).to(
|
| 702 |
+
inputs_embeds.device, inputs_embeds.dtype
|
| 703 |
+
)
|
| 704 |
+
audio_mask = self.get_placeholder_mask(
|
| 705 |
+
input_ids, inputs_embeds, self.config.audio_token_id, audio_features.shape[0]
|
| 706 |
+
)
|
| 707 |
+
inputs_embeds = inputs_embeds.masked_scatter(audio_mask, audio_features)
|
| 708 |
+
if audio_out_codes is not None:
|
| 709 |
+
audio_out_embeds = self.get_audio_out_embeds(audio_out_codes).to(inputs_embeds.device, inputs_embeds.dtype)
|
| 710 |
+
audio_out_mask = self.get_placeholder_mask(
|
| 711 |
+
input_ids, inputs_embeds, self.config.audio_out_token_id, audio_out_embeds.shape[0]
|
| 712 |
+
)
|
| 713 |
+
inputs_embeds = inputs_embeds.masked_scatter(audio_out_mask, audio_out_embeds)
|
| 714 |
+
|
| 715 |
+
outputs = self.language_model(
|
| 716 |
+
attention_mask=attention_mask,
|
| 717 |
+
position_ids=position_ids,
|
| 718 |
+
past_key_values=past_key_values,
|
| 719 |
+
inputs_embeds=inputs_embeds,
|
| 720 |
+
use_cache=use_cache,
|
| 721 |
+
**kwargs,
|
| 722 |
+
)
|
| 723 |
+
return Lfm3OmniModelOutputWithPast(
|
| 724 |
+
last_hidden_state=outputs.last_hidden_state,
|
| 725 |
+
past_key_values=outputs.past_key_values,
|
| 726 |
+
hidden_states=outputs.hidden_states,
|
| 727 |
+
attentions=outputs.attentions,
|
| 728 |
+
audio_hidden_states=audio_features,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
@auto_docstring
|
| 732 |
+
def get_image_features(self, pixel_values: torch.Tensor, image_grid_thw: torch.LongTensor) -> torch.Tensor:
|
| 733 |
+
r"""
|
| 734 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 735 |
+
Flattened image patches. `patch_dim` is channels times temporal patch size times squared spatial patch size.
|
| 736 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 737 |
+
Temporal, height, and width patch counts before spatial merging, in image placeholder order.
|
| 738 |
+
|
| 739 |
+
Returns:
|
| 740 |
+
`torch.Tensor` of shape `(num_image_tokens, hidden_size)`: Projected image features in placeholder order.
|
| 741 |
+
"""
|
| 742 |
+
vision = self.vision_tower(pixel_values, grid_thw=image_grid_thw, return_dict=True)
|
| 743 |
+
return self.multi_modal_projector(vision.last_hidden_state)
|
| 744 |
+
|
| 745 |
+
@auto_docstring
|
| 746 |
+
def get_video_features(self, pixel_values_videos: torch.Tensor, video_grid_thw: torch.LongTensor) -> torch.Tensor:
|
| 747 |
+
r"""
|
| 748 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 749 |
+
Flattened video patches; the tower encodes every temporal patch as its own spatial sequence.
|
| 750 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
|
| 751 |
+
Temporal, height, and width patch counts before spatial merging, in video placeholder order.
|
| 752 |
+
|
| 753 |
+
Returns:
|
| 754 |
+
`torch.Tensor` of shape `(num_video_tokens, hidden_size)`: Projected video features in placeholder order.
|
| 755 |
+
"""
|
| 756 |
+
return self.get_image_features(pixel_values_videos, video_grid_thw)
|
| 757 |
+
|
| 758 |
+
|
| 759 |
+
class Lfm3OmniGenerationMode(str, Enum):
|
| 760 |
+
TEXT = "text"
|
| 761 |
+
AUDIO = "audio"
|
| 762 |
+
INTERLEAVED = "interleaved"
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
@auto_docstring(
|
| 766 |
+
custom_intro="""
|
| 767 |
+
The LFM3-Omni model: an LFM3 language model that reads speech through the Liquid speech encoder, reads images
|
| 768 |
+
through the LFM3-VL vision tower and writes speech as codebook frames through a depth decoder, alongside text.
|
| 769 |
+
"""
|
| 770 |
+
)
|
| 771 |
+
class Lfm3OmniForConditionalGeneration(Lfm3OmniPreTrainedModel, GenerationMixin):
|
| 772 |
+
# Trainer counts text labels only; the joint loss also supervises speech frames.
|
| 773 |
+
accepts_loss_kwargs = False
|
| 774 |
+
_tied_weights_keys = {"lm_head.weight": "model.language_model.embed_tokens.weight"}
|
| 775 |
+
_checkpoint_conversion_mapping = {}
|
| 776 |
+
|
| 777 |
+
# `GenerationConfig` fields the text/speech mode machine does not implement, with their inert values
|
| 778 |
+
_generation_fields_unsupported = {
|
| 779 |
+
"min_length": 0,
|
| 780 |
+
"min_new_tokens": None,
|
| 781 |
+
"max_time": None,
|
| 782 |
+
"stop_strings": None,
|
| 783 |
+
"early_stopping": False,
|
| 784 |
+
"penalty_alpha": None,
|
| 785 |
+
"dola_layers": None,
|
| 786 |
+
"use_cache": True,
|
| 787 |
+
"cache_implementation": None,
|
| 788 |
+
"cache_config": None,
|
| 789 |
+
"return_legacy_cache": None,
|
| 790 |
+
"prefill_chunk_size": None,
|
| 791 |
+
"typical_p": 1.0,
|
| 792 |
+
"epsilon_cutoff": 0.0,
|
| 793 |
+
"eta_cutoff": 0.0,
|
| 794 |
+
"diversity_penalty": 0.0,
|
| 795 |
+
"length_penalty": 1.0,
|
| 796 |
+
"no_repeat_ngram_size": 0,
|
| 797 |
+
"bad_words_ids": None,
|
| 798 |
+
"force_words_ids": None,
|
| 799 |
+
"renormalize_logits": False,
|
| 800 |
+
"remove_invalid_values": False,
|
| 801 |
+
"top_h": None,
|
| 802 |
+
"encoder_repetition_penalty": 1.0,
|
| 803 |
+
"use_mtp": None,
|
| 804 |
+
"constraints": None,
|
| 805 |
+
"forced_bos_token_id": None,
|
| 806 |
+
"forced_eos_token_id": None,
|
| 807 |
+
"exponential_decay_length_penalty": None,
|
| 808 |
+
"suppress_tokens": None,
|
| 809 |
+
"begin_suppress_tokens": None,
|
| 810 |
+
"sequence_bias": None,
|
| 811 |
+
"token_healing": False,
|
| 812 |
+
"guidance_scale": None,
|
| 813 |
+
"watermarking_config": None,
|
| 814 |
+
"output_attentions": False,
|
| 815 |
+
"output_hidden_states": False,
|
| 816 |
+
"output_scores": False,
|
| 817 |
+
"output_logits": None,
|
| 818 |
+
"encoder_no_repeat_ngram_size": 0,
|
| 819 |
+
"prompt_lookup_num_tokens": None,
|
| 820 |
+
"assistant_early_exit": None,
|
| 821 |
+
"compile_config": None,
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
def __init__(self, config: Lfm3OmniConfig):
|
| 825 |
+
super().__init__(config)
|
| 826 |
+
self.model = Lfm3OmniModel(config)
|
| 827 |
+
self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False)
|
| 828 |
+
self.post_init()
|
| 829 |
+
|
| 830 |
+
def get_input_embeddings(self):
|
| 831 |
+
return self.model.get_input_embeddings()
|
| 832 |
+
|
| 833 |
+
def set_input_embeddings(self, value):
|
| 834 |
+
self.model.set_input_embeddings(value)
|
| 835 |
+
|
| 836 |
+
def get_output_embeddings(self) -> nn.Module:
|
| 837 |
+
return self.lm_head
|
| 838 |
+
|
| 839 |
+
@auto_docstring
|
| 840 |
+
def get_audio_features(self, input_features: torch.FloatTensor, feature_attention_mask: torch.Tensor):
|
| 841 |
+
r"""
|
| 842 |
+
input_features (`torch.FloatTensor` of shape `(num_clips, num_mel_bins, num_frames)`):
|
| 843 |
+
The log-mel spectrograms of the input clips, right-padded to the longest clip.
|
| 844 |
+
feature_attention_mask (`torch.Tensor` of shape `(num_clips, num_frames)`):
|
| 845 |
+
The mask of the valid spectrogram frames of each clip.
|
| 846 |
+
"""
|
| 847 |
+
return self.model.get_audio_features(input_features, feature_attention_mask)
|
| 848 |
+
|
| 849 |
+
@property
|
| 850 |
+
def language_model(self):
|
| 851 |
+
return self.model.language_model
|
| 852 |
+
|
| 853 |
+
@property
|
| 854 |
+
def _placeholder_token_ids(self) -> list[int]:
|
| 855 |
+
return [
|
| 856 |
+
self.config.audio_token_id,
|
| 857 |
+
self.config.audio_out_token_id,
|
| 858 |
+
self.config.image_token_id,
|
| 859 |
+
self.config.video_token_id,
|
| 860 |
+
]
|
| 861 |
+
|
| 862 |
+
@property
|
| 863 |
+
def depth_decoder(self):
|
| 864 |
+
return self.model.depth_decoder
|
| 865 |
+
|
| 866 |
+
@can_return_tuple
|
| 867 |
+
@auto_docstring
|
| 868 |
+
def forward(
|
| 869 |
+
self,
|
| 870 |
+
input_ids: torch.LongTensor | None = None,
|
| 871 |
+
attention_mask: torch.Tensor | None = None,
|
| 872 |
+
position_ids: torch.LongTensor | None = None,
|
| 873 |
+
past_key_values: Cache | None = None,
|
| 874 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 875 |
+
input_features: torch.FloatTensor | None = None,
|
| 876 |
+
feature_attention_mask: torch.Tensor | None = None,
|
| 877 |
+
audio_out_codes: torch.LongTensor | None = None,
|
| 878 |
+
labels: torch.LongTensor | None = None,
|
| 879 |
+
audio_labels: torch.LongTensor | None = None,
|
| 880 |
+
pixel_values: torch.FloatTensor | None = None,
|
| 881 |
+
image_grid_thw: torch.LongTensor | None = None,
|
| 882 |
+
pixel_values_videos: torch.FloatTensor | None = None,
|
| 883 |
+
video_grid_thw: torch.LongTensor | None = None,
|
| 884 |
+
use_cache: bool | None = None,
|
| 885 |
+
logits_to_keep: int | torch.Tensor = 0,
|
| 886 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 887 |
+
) -> tuple | Lfm3OmniCausalLMOutputWithPast:
|
| 888 |
+
r"""
|
| 889 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 890 |
+
Flattened image patches. `patch_dim` is channels times temporal patch size times squared spatial patch size.
|
| 891 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 892 |
+
Flattened video patches; the tower encodes every temporal patch as its own spatial sequence.
|
| 893 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
|
| 894 |
+
Temporal, height, and width patch counts before spatial merging, in video placeholder order.
|
| 895 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 896 |
+
Temporal, height, and width patch counts before spatial merging, in image placeholder order.
|
| 897 |
+
input_features (`torch.FloatTensor` of shape `(num_clips, num_mel_bins, num_frames)`, *optional*):
|
| 898 |
+
The log-mel spectrograms of the input clips, right-padded to the longest clip.
|
| 899 |
+
feature_attention_mask (`torch.Tensor` of shape `(num_clips, num_frames)`, *optional*):
|
| 900 |
+
The mask of the valid spectrogram frames of each clip.
|
| 901 |
+
audio_out_codes (`torch.LongTensor` of shape `(num_frames, num_codebooks)`, *optional*):
|
| 902 |
+
The codes of the speech frames in the sequence, one row per `audio_out_token_id` placeholder. A speech
|
| 903 |
+
placeholder without its features or codes is refused, except in a compiled or captured forward, which
|
| 904 |
+
skips the check.
|
| 905 |
+
labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
|
| 906 |
+
Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
|
| 907 |
+
config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
|
| 908 |
+
(masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
|
| 909 |
+
audio_labels (`torch.LongTensor` of shape `(num_frames, num_codebooks)`, *optional*):
|
| 910 |
+
The codes of the speech frames to learn, one row per `audio_out_token_id` placeholder; rows of `-100` are
|
| 911 |
+
ignored. The depth decoder predicts a random subset of each frame's codebooks from the others and the
|
| 912 |
+
hidden state preceding the frame.
|
| 913 |
+
"""
|
| 914 |
+
outputs = self.model(
|
| 915 |
+
input_ids=input_ids,
|
| 916 |
+
attention_mask=attention_mask,
|
| 917 |
+
position_ids=position_ids,
|
| 918 |
+
past_key_values=past_key_values,
|
| 919 |
+
inputs_embeds=inputs_embeds,
|
| 920 |
+
input_features=input_features,
|
| 921 |
+
feature_attention_mask=feature_attention_mask,
|
| 922 |
+
audio_out_codes=audio_out_codes,
|
| 923 |
+
pixel_values=pixel_values,
|
| 924 |
+
image_grid_thw=image_grid_thw,
|
| 925 |
+
pixel_values_videos=pixel_values_videos,
|
| 926 |
+
video_grid_thw=video_grid_thw,
|
| 927 |
+
use_cache=use_cache,
|
| 928 |
+
**kwargs,
|
| 929 |
+
)
|
| 930 |
+
hidden_states = outputs[0]
|
| 931 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 932 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 933 |
+
|
| 934 |
+
loss = text_loss = audio_loss = None
|
| 935 |
+
if labels is not None:
|
| 936 |
+
text_loss = (
|
| 937 |
+
self.loss_function(
|
| 938 |
+
logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size, **kwargs
|
| 939 |
+
)
|
| 940 |
+
if (labels[..., 1:] != -100).any()
|
| 941 |
+
else logits.sum() * 0.0
|
| 942 |
+
)
|
| 943 |
+
if audio_labels is not None:
|
| 944 |
+
if input_ids is None:
|
| 945 |
+
raise ValueError("`audio_labels` requires `input_ids` to locate the speech frames")
|
| 946 |
+
audio_loss = self.audio_loss(input_ids, hidden_states, audio_labels)
|
| 947 |
+
|
| 948 |
+
if text_loss is not None or audio_loss is not None:
|
| 949 |
+
loss = self.combined_loss(input_ids, labels, audio_labels, text_loss, audio_loss)
|
| 950 |
+
|
| 951 |
+
return Lfm3OmniCausalLMOutputWithPast(
|
| 952 |
+
loss=loss,
|
| 953 |
+
text_loss=text_loss,
|
| 954 |
+
audio_loss=audio_loss,
|
| 955 |
+
logits=logits,
|
| 956 |
+
past_key_values=outputs.past_key_values,
|
| 957 |
+
hidden_states=outputs.hidden_states,
|
| 958 |
+
attentions=outputs.attentions,
|
| 959 |
+
audio_hidden_states=outputs.audio_hidden_states,
|
| 960 |
+
)
|
| 961 |
+
|
| 962 |
+
def audio_loss(
|
| 963 |
+
self, input_ids: torch.LongTensor, hidden_states: torch.FloatTensor, audio_labels: torch.LongTensor
|
| 964 |
+
) -> torch.FloatTensor:
|
| 965 |
+
config = self.config
|
| 966 |
+
# under `device_map` sharding the last hidden states sit on the last layer's device, not the inputs'
|
| 967 |
+
input_ids, audio_labels = input_ids.to(hidden_states.device), audio_labels.to(hidden_states.device)
|
| 968 |
+
placeholders = input_ids == config.audio_out_token_id
|
| 969 |
+
if audio_labels.shape != (int(placeholders.sum()), config.num_codebooks):
|
| 970 |
+
raise ValueError(
|
| 971 |
+
f"Expected audio_labels of shape (num_frames, {config.num_codebooks}), one row per "
|
| 972 |
+
f"`audio_out_token_id` placeholder, got {tuple(audio_labels.shape)}"
|
| 973 |
+
)
|
| 974 |
+
# rows of -100 are ignored; any other code indexes the codebook embeddings and the loss
|
| 975 |
+
ignored = (audio_labels == -100).all(-1, keepdim=True)
|
| 976 |
+
torch_compilable_check(
|
| 977 |
+
ignored | ((audio_labels >= 0) & (audio_labels < config.codebook_size)),
|
| 978 |
+
f"audio_labels must be in [0, {config.codebook_size}) or rows of -100",
|
| 979 |
+
)
|
| 980 |
+
# a frame is predicted from the position before it, so the first position of a row has no frame
|
| 981 |
+
preceding = placeholders.clone()
|
| 982 |
+
preceding[:, 0] = False
|
| 983 |
+
supervised = (audio_labels != -100).all(-1)[preceding[placeholders]]
|
| 984 |
+
latents = hidden_states[preceding.roll(-1, dims=1)][supervised]
|
| 985 |
+
codes = audio_labels[preceding[placeholders]][supervised]
|
| 986 |
+
if codes.shape[0] == 0:
|
| 987 |
+
return hidden_states.sum() * 0.0
|
| 988 |
+
ratio = torch.rand(codes.shape[0], 1, device=codes.device)
|
| 989 |
+
masked = torch.rand(codes.shape, device=codes.device) < ratio
|
| 990 |
+
masked |= ~masked.any(-1, keepdim=True)
|
| 991 |
+
masked |= (codes == config.audio_eos_code).all(-1, keepdim=True)
|
| 992 |
+
logits = self.model.depth_decoder(latents, codes, masked)
|
| 993 |
+
# under `device_map` sharding the depth decoder answers on its own device
|
| 994 |
+
codes, masked = codes.to(logits.device), masked.to(logits.device)
|
| 995 |
+
losses = F.cross_entropy(logits.transpose(1, 2).float(), codes, reduction="none")
|
| 996 |
+
return (losses * masked).sum() / masked.sum().clamp_min(1)
|
| 997 |
+
|
| 998 |
+
def combined_loss(self, input_ids, labels, audio_labels, text_loss, audio_loss) -> torch.Tensor:
|
| 999 |
+
"""Pooled mean over the supervised text tokens and speech frames, each modality's mean weighted by its
|
| 1000 |
+
count and its configured weight, as in training."""
|
| 1001 |
+
# under `device_map` sharding the labels and the two losses can sit on three devices
|
| 1002 |
+
device = (text_loss if text_loss is not None else audio_loss).device
|
| 1003 |
+
text_count = 0 if labels is None else (labels[..., 1:] != -100).sum().to(device)
|
| 1004 |
+
audio_count = 0
|
| 1005 |
+
if audio_labels is not None:
|
| 1006 |
+
placeholders = input_ids.to(audio_labels.device) == self.config.audio_out_token_id
|
| 1007 |
+
preceding = placeholders.clone()
|
| 1008 |
+
preceding[:, 0] = False
|
| 1009 |
+
audio_count = (audio_labels != -100).all(-1)[preceding[placeholders]].sum().to(device)
|
| 1010 |
+
text_weight = self.config.text_loss_weight * text_count
|
| 1011 |
+
audio_weight = self.config.audio_loss_weight * audio_count
|
| 1012 |
+
return (
|
| 1013 |
+
text_weight * (text_loss if text_loss is not None else 0.0)
|
| 1014 |
+
+ audio_weight * (audio_loss.to(device) if audio_loss is not None else 0.0)
|
| 1015 |
+
) / (text_weight + audio_weight + 1e-6)
|
| 1016 |
+
|
| 1017 |
+
@staticmethod
|
| 1018 |
+
def _build_warpers(
|
| 1019 |
+
temperature: float | None, top_k: int | None, top_p: float | None = None, min_p: float | None = None
|
| 1020 |
+
) -> list:
|
| 1021 |
+
"""The logits warpers of one sampling setting, empty for greedy decoding; building them validates the values."""
|
| 1022 |
+
# an explicit zero temperature is greedy; without a temperature, top_k alone samples at unit temperature
|
| 1023 |
+
# like the training reference
|
| 1024 |
+
if temperature == 0 or (temperature is None and not top_k):
|
| 1025 |
+
return []
|
| 1026 |
+
warpers = []
|
| 1027 |
+
if temperature:
|
| 1028 |
+
warpers.append(TemperatureLogitsWarper(float(temperature)))
|
| 1029 |
+
if top_k:
|
| 1030 |
+
warpers.append(TopKLogitsWarper(int(top_k)))
|
| 1031 |
+
if top_p is not None and top_p < 1.0:
|
| 1032 |
+
warpers.append(TopPLogitsWarper(float(top_p)))
|
| 1033 |
+
if min_p:
|
| 1034 |
+
warpers.append(MinPLogitsWarper(float(min_p)))
|
| 1035 |
+
return warpers
|
| 1036 |
+
|
| 1037 |
+
@classmethod
|
| 1038 |
+
def _sample(
|
| 1039 |
+
cls,
|
| 1040 |
+
logits: torch.Tensor,
|
| 1041 |
+
temperature: float | None,
|
| 1042 |
+
top_k: int | None,
|
| 1043 |
+
repetition_penalty: float | None = None,
|
| 1044 |
+
history: torch.Tensor | None = None,
|
| 1045 |
+
suppress: list[int] = (),
|
| 1046 |
+
top_p: float | None = None,
|
| 1047 |
+
min_p: float | None = None,
|
| 1048 |
+
warpers: list | None = None,
|
| 1049 |
+
) -> torch.Tensor:
|
| 1050 |
+
scores = logits.float()[None].clone()
|
| 1051 |
+
if repetition_penalty is not None and repetition_penalty != 1.0 and history is not None:
|
| 1052 |
+
penalty = RepetitionPenaltyLogitsProcessor(float(repetition_penalty))
|
| 1053 |
+
scores = penalty(history.reshape(1, -1).to(scores.device), scores)
|
| 1054 |
+
if suppress:
|
| 1055 |
+
scores[:, list(suppress)] = -float("inf")
|
| 1056 |
+
if warpers is None:
|
| 1057 |
+
warpers = cls._build_warpers(temperature, top_k, top_p, min_p)
|
| 1058 |
+
if not warpers:
|
| 1059 |
+
return scores.argmax(-1)
|
| 1060 |
+
for warper in warpers:
|
| 1061 |
+
scores = warper(None, scores)
|
| 1062 |
+
return torch.multinomial(scores.softmax(-1), 1)[0]
|
| 1063 |
+
|
| 1064 |
+
def sample_audio_frame(
|
| 1065 |
+
self,
|
| 1066 |
+
hidden_state: torch.Tensor,
|
| 1067 |
+
temperature: float | None = None,
|
| 1068 |
+
top_k: int | None = None,
|
| 1069 |
+
repetition_penalty: float | None = None,
|
| 1070 |
+
history: torch.Tensor | list[torch.Tensor | None] | None = None,
|
| 1071 |
+
warpers: list | None = None,
|
| 1072 |
+
) -> torch.LongTensor:
|
| 1073 |
+
r"""
|
| 1074 |
+
Samples the codes of one speech frame from a language model hidden state of shape `(hidden_size,)` or `(batch_size, hidden_size)`: the
|
| 1075 |
+
codebooks are revealed in a random order over the passes of `config.maskgit_schedule`, each pass scoring the
|
| 1076 |
+
codebooks still hidden from the depth decoder. `history` holds the previous frames, `(num_frames,
|
| 1077 |
+
num_codebooks)`, for the repetition penalty, or a list of per-row histories for a batch. `warpers` are the
|
| 1078 |
+
logits warpers of `temperature` and `top_k`, prebuilt by `_build_warpers`.
|
| 1079 |
+
"""
|
| 1080 |
+
single = hidden_state.ndim == 1
|
| 1081 |
+
if single:
|
| 1082 |
+
hidden_state = hidden_state[None]
|
| 1083 |
+
history = [history]
|
| 1084 |
+
batch_size = hidden_state.shape[0]
|
| 1085 |
+
if history is None:
|
| 1086 |
+
history = [None] * batch_size
|
| 1087 |
+
num_codebooks = self.config.num_codebooks
|
| 1088 |
+
codes = hidden_state.new_zeros((batch_size, num_codebooks), dtype=torch.long)
|
| 1089 |
+
revealed = torch.zeros_like(codes, dtype=torch.bool)
|
| 1090 |
+
order = torch.stack([torch.randperm(num_codebooks, device=hidden_state.device) for _ in range(batch_size)])
|
| 1091 |
+
for pick in order.split(self.config.maskgit_schedule, dim=1):
|
| 1092 |
+
# the depth decoder's forward keeps every pass on its own device under `device_map` sharding or offload
|
| 1093 |
+
logits = self.model.depth_decoder(hidden_state, codes, ~revealed)
|
| 1094 |
+
for row in range(batch_size):
|
| 1095 |
+
for index in pick[row].tolist():
|
| 1096 |
+
codebook_history = None if history[row] is None else history[row][:, index]
|
| 1097 |
+
codes[row, index] = self._sample(
|
| 1098 |
+
logits[row, index], temperature, top_k, repetition_penalty, codebook_history, warpers=warpers
|
| 1099 |
+
).to(codes.device)
|
| 1100 |
+
revealed.scatter_(1, pick, True)
|
| 1101 |
+
return codes[0] if single else codes
|
| 1102 |
+
|
| 1103 |
+
@torch.no_grad()
|
| 1104 |
+
def prefill_audio(
|
| 1105 |
+
self,
|
| 1106 |
+
input_features: torch.FloatTensor,
|
| 1107 |
+
past_key_values: Cache | None = None,
|
| 1108 |
+
encoder_cache: Lfm3OmniEncoderStreamingCache | None = None,
|
| 1109 |
+
attention_mask: torch.Tensor | None = None,
|
| 1110 |
+
is_final: bool = False,
|
| 1111 |
+
) -> Lfm3OmniStreamingOutput:
|
| 1112 |
+
"""
|
| 1113 |
+
Encode a mel chunk and append completed audio tokens to the language-model cache.
|
| 1114 |
+
|
| 1115 |
+
Args:
|
| 1116 |
+
input_features (`torch.FloatTensor` of shape `(batch_size, num_mel_bins, num_frames)`):
|
| 1117 |
+
New mel frames, without padding. Batched streams must have synchronized chunk lengths.
|
| 1118 |
+
past_key_values (`Cache`, *optional*):
|
| 1119 |
+
Cache from a text-prefix forward pass or the preceding `prefill_audio` call. Updated in place.
|
| 1120 |
+
encoder_cache (`Lfm3OmniEncoderStreamingCache`, *optional*):
|
| 1121 |
+
Encoder state from the preceding call. Omit at the start of each new utterance.
|
| 1122 |
+
attention_mask (`torch.Tensor` of shape `(batch_size, cached_sequence_length)`, *optional*):
|
| 1123 |
+
Mask of the tokens already in `past_key_values`, required for a nonempty cache. Pass the returned
|
| 1124 |
+
mask to the next call. Audio tokens are appended as valid positions.
|
| 1125 |
+
is_final (`bool`, *optional*, defaults to `False`):
|
| 1126 |
+
Flush the encoder's trailing token and finalize this utterance, including for an empty final chunk.
|
| 1127 |
+
|
| 1128 |
+
Returns:
|
| 1129 |
+
[`Lfm3OmniStreamingOutput`]: Updated encoder state, language-model cache and full attention mask.
|
| 1130 |
+
Chunks that complete no token leave the language-model cache unchanged. After finalization, pass the
|
| 1131 |
+
closing audio/user delimiters and assistant prefix to `generate` with this cache and a mask extended
|
| 1132 |
+
for those new tokens. Returned generation sequences include only that suffix and the generated tokens.
|
| 1133 |
+
"""
|
| 1134 |
+
if self.training:
|
| 1135 |
+
raise ValueError("Streaming audio prefill requires eval mode")
|
| 1136 |
+
if input_features.ndim != 3 or input_features.shape[1] != self.config.audio_encoder_config.num_mel_bins:
|
| 1137 |
+
raise ValueError("Expected features of shape (batch_size, num_mel_bins, num_frames)")
|
| 1138 |
+
batch_size = input_features.shape[0]
|
| 1139 |
+
past_length = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 1140 |
+
if attention_mask is None:
|
| 1141 |
+
if past_length:
|
| 1142 |
+
raise ValueError("Pass attention_mask covering the tokens already in past_key_values")
|
| 1143 |
+
attention_mask = torch.ones((batch_size, 0), dtype=torch.long, device=input_features.device)
|
| 1144 |
+
if attention_mask.shape != (batch_size, past_length):
|
| 1145 |
+
raise ValueError("attention_mask must cover exactly the tokens already in past_key_values")
|
| 1146 |
+
not_binary = ((attention_mask != 0) & (attention_mask != 1)).any()
|
| 1147 |
+
not_monotonic = ((attention_mask[:, 1:].long() - attention_mask[:, :-1].long()) < 0).any()
|
| 1148 |
+
if bool(not_binary | not_monotonic):
|
| 1149 |
+
raise ValueError("attention_mask must describe contiguous left-padded prompts")
|
| 1150 |
+
if past_length and not bool(attention_mask[:, -1].all()):
|
| 1151 |
+
raise ValueError("Each cached prompt must end in a valid token")
|
| 1152 |
+
if past_key_values is None:
|
| 1153 |
+
past_key_values = DynamicCache(config=self.config.text_config)
|
| 1154 |
+
embeddings, encoder_cache = self.model.get_audio_features_stream(input_features, encoder_cache, is_final)
|
| 1155 |
+
num_tokens = embeddings.shape[1]
|
| 1156 |
+
hidden_states = None
|
| 1157 |
+
if num_tokens:
|
| 1158 |
+
attention_mask = torch.cat((attention_mask, attention_mask.new_ones((batch_size, num_tokens))), dim=-1)
|
| 1159 |
+
position_ids = attention_mask.long().cumsum(-1).sub(1).clamp_min(0)[:, -num_tokens:]
|
| 1160 |
+
outputs = self.model.language_model(
|
| 1161 |
+
inputs_embeds=embeddings,
|
| 1162 |
+
past_key_values=past_key_values,
|
| 1163 |
+
attention_mask=attention_mask,
|
| 1164 |
+
position_ids=position_ids,
|
| 1165 |
+
use_cache=True,
|
| 1166 |
+
)
|
| 1167 |
+
hidden_states, past_key_values = outputs.last_hidden_state, outputs.past_key_values
|
| 1168 |
+
return Lfm3OmniStreamingOutput(
|
| 1169 |
+
past_key_values=past_key_values,
|
| 1170 |
+
encoder_cache=encoder_cache,
|
| 1171 |
+
attention_mask=attention_mask,
|
| 1172 |
+
last_hidden_state=hidden_states,
|
| 1173 |
+
)
|
| 1174 |
+
|
| 1175 |
+
def _live_forward(self, state: Lfm3OmniLiveTranscriptionState, inputs_embeds: torch.FloatTensor) -> torch.Tensor:
|
| 1176 |
+
past_length = state.attention_mask.shape[1]
|
| 1177 |
+
state.attention_mask = torch.cat(
|
| 1178 |
+
(state.attention_mask, state.attention_mask.new_ones((1, inputs_embeds.shape[1]))), dim=-1
|
| 1179 |
+
)
|
| 1180 |
+
position_ids = torch.arange(past_length, state.attention_mask.shape[1], device=inputs_embeds.device)[None]
|
| 1181 |
+
outputs = self.model.language_model(
|
| 1182 |
+
inputs_embeds=inputs_embeds,
|
| 1183 |
+
past_key_values=state.past_key_values,
|
| 1184 |
+
attention_mask=state.attention_mask,
|
| 1185 |
+
position_ids=position_ids,
|
| 1186 |
+
use_cache=True,
|
| 1187 |
+
)
|
| 1188 |
+
return outputs.last_hidden_state[:, -1]
|
| 1189 |
+
|
| 1190 |
+
@torch.no_grad()
|
| 1191 |
+
def transcribe_live(
|
| 1192 |
+
self,
|
| 1193 |
+
input_features: torch.FloatTensor,
|
| 1194 |
+
state: Lfm3OmniLiveTranscriptionState | None = None,
|
| 1195 |
+
input_ids: torch.LongTensor | None = None,
|
| 1196 |
+
is_final: bool = False,
|
| 1197 |
+
max_new_tokens: int = 48,
|
| 1198 |
+
suppress_token_ids: list[int] | None = None,
|
| 1199 |
+
stop_token_ids: list[int] | None = None,
|
| 1200 |
+
) -> Lfm3OmniLiveTranscriptionOutput:
|
| 1201 |
+
"""
|
| 1202 |
+
Transcribe one chunk of a live stream with a checkpoint trained for live ASR.
|
| 1203 |
+
|
| 1204 |
+
The audio goes inside the assistant turn in chunks of `config.interleaved_n_audio` encoder tokens, each
|
| 1205 |
+
followed by the text it completes:
|
| 1206 |
+
|
| 1207 |
+
<prompt> [A x 12] text<|text_end|> [A x 12] text<|text_end|> ... [A x r] <|mixed_end|> text<|text_end|><|im_end|>
|
| 1208 |
+
|
| 1209 |
+
Args:
|
| 1210 |
+
input_features (`torch.FloatTensor` of shape `(1, num_mel_bins, num_frames)`):
|
| 1211 |
+
The chunk's new mel frames, normally `8 * config.interleaved_n_audio`. A chunk that completes no
|
| 1212 |
+
audio token decodes no text.
|
| 1213 |
+
state ([`Lfm3OmniLiveTranscriptionState`], *optional*):
|
| 1214 |
+
The state returned by the previous call. Omit for a new stream.
|
| 1215 |
+
input_ids (`torch.LongTensor` of shape `(1, prompt_length)`, *optional*):
|
| 1216 |
+
The prompt of a new language-model context (the system turn and the assistant header), required
|
| 1217 |
+
for a new stream. With a `state`, it starts the context over while the encoder keeps following the
|
| 1218 |
+
stream, the way training samples of at most 30 seconds are chained.
|
| 1219 |
+
is_final (`bool`, *optional*, defaults to `False`):
|
| 1220 |
+
Flush the encoder and decode the closing block, which ends at `<|text_end|>` or a stop token.
|
| 1221 |
+
max_new_tokens (`int`, *optional*, defaults to 48):
|
| 1222 |
+
Text budget of the block; a block that exhausts it is still closed with `<|text_end|>`.
|
| 1223 |
+
suppress_token_ids (`list[int]`, *optional*):
|
| 1224 |
+
Tokens never valid before the final chunk, on top of `<|audio_start|>`, `<|mixed_start|>` and the
|
| 1225 |
+
end-of-turn token: pass `<|mixed_end|>` and `<|endoftext|>`.
|
| 1226 |
+
stop_token_ids (`list[int]`, *optional*):
|
| 1227 |
+
Tokens ending the final block on top of the end-of-turn token: pass `<|endoftext|>` and `<|im_start|>`.
|
| 1228 |
+
|
| 1229 |
+
Returns:
|
| 1230 |
+
[`Lfm3OmniLiveTranscriptionOutput`]: the chunk's text tokens (the final block starts with
|
| 1231 |
+
`<|mixed_end|>`), the number of audio tokens it completed, and the updated state.
|
| 1232 |
+
"""
|
| 1233 |
+
if self.training:
|
| 1234 |
+
raise ValueError("Live transcription requires eval mode")
|
| 1235 |
+
num_mel_bins = self.config.audio_encoder_config.num_mel_bins
|
| 1236 |
+
if input_features.ndim != 3 or input_features.shape[0] != 1 or input_features.shape[1] != num_mel_bins:
|
| 1237 |
+
raise ValueError(
|
| 1238 |
+
f"Live transcription takes one stream: features of shape (1, {num_mel_bins}, num_frames), got "
|
| 1239 |
+
f"{tuple(input_features.shape)}"
|
| 1240 |
+
)
|
| 1241 |
+
if input_ids is not None:
|
| 1242 |
+
if input_ids.ndim != 2 or input_ids.shape[0] != 1:
|
| 1243 |
+
raise ValueError("Live transcription takes one prompt of shape (1, prompt_length)")
|
| 1244 |
+
self.model.check_speech_placeholders(input_ids, None, live=True)
|
| 1245 |
+
if state is None:
|
| 1246 |
+
if input_ids is None:
|
| 1247 |
+
raise ValueError("A new stream needs the prompt `input_ids`")
|
| 1248 |
+
state = Lfm3OmniLiveTranscriptionState()
|
| 1249 |
+
if state.finished:
|
| 1250 |
+
raise ValueError("This stream is finalized; start with a new state")
|
| 1251 |
+
eos_token_ids = self.config.text_config.eos_token_id
|
| 1252 |
+
eos_token_ids = (
|
| 1253 |
+
[] if eos_token_ids is None else [eos_token_ids] if isinstance(eos_token_ids, int) else list(eos_token_ids)
|
| 1254 |
+
)
|
| 1255 |
+
text_end_token_id = self.config.text_end_token_id
|
| 1256 |
+
suppress = [
|
| 1257 |
+
token_id
|
| 1258 |
+
for token_id in (
|
| 1259 |
+
self.config.audio_start_token_id,
|
| 1260 |
+
self.config.mixed_start_token_id,
|
| 1261 |
+
*eos_token_ids,
|
| 1262 |
+
*(suppress_token_ids or []),
|
| 1263 |
+
)
|
| 1264 |
+
# `<|text_end|>` closes every block, so it is never suppressed even when it is listed as an end-of-turn id
|
| 1265 |
+
if token_id is not None and token_id != text_end_token_id
|
| 1266 |
+
]
|
| 1267 |
+
# the ids index the logits; refused before the encoder cache advances
|
| 1268 |
+
vocab_size = self.config.text_config.vocab_size
|
| 1269 |
+
if any(not 0 <= token_id < vocab_size for token_id in suppress):
|
| 1270 |
+
raise ValueError(f"Suppressed token ids must lie in [0, {vocab_size}), got {suppress}")
|
| 1271 |
+
stop = {*eos_token_ids, *(stop_token_ids or [])}
|
| 1272 |
+
# the encoder runs before a restart replaces the context, so a rejected chunk leaves the context intact
|
| 1273 |
+
embeddings, state.encoder_cache = self.model.get_audio_features_stream(
|
| 1274 |
+
input_features, state.encoder_cache, is_final
|
| 1275 |
+
)
|
| 1276 |
+
embed_tokens = self.get_input_embeddings()
|
| 1277 |
+
# an offloaded table sits on `meta`: the device comes from the embedding output it is joined with
|
| 1278 |
+
embeddings = embeddings.to(dtype=embed_tokens.weight.dtype)
|
| 1279 |
+
if input_ids is not None:
|
| 1280 |
+
state.past_key_values = DynamicCache(config=self.config.text_config)
|
| 1281 |
+
state.attention_mask = torch.zeros((1, 0), dtype=torch.long, device=input_features.device)
|
| 1282 |
+
state.pending = embed_tokens(input_ids)
|
| 1283 |
+
num_audio_tokens = embeddings.shape[1]
|
| 1284 |
+
generated, closed = [], False
|
| 1285 |
+
if num_audio_tokens or is_final:
|
| 1286 |
+
# the pending text goes in with the audio it precedes, so a chunk completing no token leaves it pending
|
| 1287 |
+
inputs_embeds = (
|
| 1288 |
+
embeddings
|
| 1289 |
+
if state.pending is None
|
| 1290 |
+
else torch.cat((state.pending, embeddings.to(state.pending.device)), dim=1)
|
| 1291 |
+
)
|
| 1292 |
+
logits = self.lm_head(self._live_forward(state, inputs_embeds))
|
| 1293 |
+
state.pending = None
|
| 1294 |
+
# the placeholders are masked on every chunk, the final one included
|
| 1295 |
+
placeholder_ids = torch.tensor(self._placeholder_token_ids, device=logits.device)
|
| 1296 |
+
suppress = torch.tensor(suppress, dtype=torch.long, device=logits.device)
|
| 1297 |
+
for _ in range(max_new_tokens):
|
| 1298 |
+
logits[:, placeholder_ids] = float("-inf")
|
| 1299 |
+
if not is_final:
|
| 1300 |
+
logits[:, suppress] = float("-inf")
|
| 1301 |
+
next_token = logits.argmax(-1, keepdim=True)
|
| 1302 |
+
token = int(next_token)
|
| 1303 |
+
if token == text_end_token_id or (is_final and token in stop):
|
| 1304 |
+
closed = True
|
| 1305 |
+
break
|
| 1306 |
+
generated.append(token)
|
| 1307 |
+
logits = self.lm_head(self._live_forward(state, embed_tokens(next_token)))
|
| 1308 |
+
if is_final:
|
| 1309 |
+
state.finished = True
|
| 1310 |
+
else:
|
| 1311 |
+
# the block closes with `<|text_end|>`, fed together with the next chunk's audio
|
| 1312 |
+
state.pending = embed_tokens(torch.tensor([[text_end_token_id]], device=input_features.device))
|
| 1313 |
+
return Lfm3OmniLiveTranscriptionOutput(
|
| 1314 |
+
sequences=torch.tensor([generated], dtype=torch.long, device=input_features.device),
|
| 1315 |
+
num_audio_tokens=num_audio_tokens,
|
| 1316 |
+
closed=closed,
|
| 1317 |
+
state=state,
|
| 1318 |
+
)
|
| 1319 |
+
|
| 1320 |
+
@torch.no_grad()
|
| 1321 |
+
def generate(
|
| 1322 |
+
self,
|
| 1323 |
+
input_ids: torch.LongTensor,
|
| 1324 |
+
input_features: torch.FloatTensor | None = None,
|
| 1325 |
+
feature_attention_mask: torch.Tensor | None = None,
|
| 1326 |
+
audio_out_codes: torch.LongTensor | None = None,
|
| 1327 |
+
generation_config: GenerationConfig | None = None,
|
| 1328 |
+
generation_mode: Lfm3OmniGenerationMode | str | list[str] | None = None,
|
| 1329 |
+
audio_temperature: float | None = None,
|
| 1330 |
+
audio_top_k: int | None = None,
|
| 1331 |
+
audio_repetition_penalty: float | None = None,
|
| 1332 |
+
audio_repetition_window: int = 64,
|
| 1333 |
+
text_repetition_window: int = 32,
|
| 1334 |
+
suppress_mixed: bool = False,
|
| 1335 |
+
suppress_audio_out: bool = False,
|
| 1336 |
+
return_audio_eos: bool = False,
|
| 1337 |
+
streamer: BaseStreamer | list[BaseStreamer | None] | None = None,
|
| 1338 |
+
audio_streamer: BaseStreamer | list[BaseStreamer | None] | None = None,
|
| 1339 |
+
past_key_values: Cache | None = None,
|
| 1340 |
+
**kwargs,
|
| 1341 |
+
) -> Lfm3OmniGenerateOutput | torch.LongTensor:
|
| 1342 |
+
r"""
|
| 1343 |
+
Generates text and speech for a batch of left-padded prompts.
|
| 1344 |
+
|
| 1345 |
+
Text is the default output. `<|audio_start|>` switches to speech until the depth decoder emits an
|
| 1346 |
+
end-of-audio frame, `<|mixed_start|>` to blocks of `config.interleaved_n_text` text tokens and
|
| 1347 |
+
`config.interleaved_n_audio` speech frames until `<|text_end|>` closes the text and the remaining speech
|
| 1348 |
+
ends with an end-of-audio frame. A prompt ending on `<|audio_start|>`, a speech frame or `<|mixed_start|>`
|
| 1349 |
+
starts in that mode. Generation stops at `<|im_end|>` emitted in text mode or after `max_new_tokens`
|
| 1350 |
+
generated slots, a speech frame counting as one slot.
|
| 1351 |
+
|
| 1352 |
+
Args:
|
| 1353 |
+
input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
|
| 1354 |
+
The uncached prompt tokens, with one `audio_token_id` per encoded frame of each input clip and one
|
| 1355 |
+
`audio_out_token_id` per speech frame. With `past_key_values`, supply only the new suffix; returned
|
| 1356 |
+
sequences contain this suffix and generated tokens, excluding the cached prefix.
|
| 1357 |
+
input_features (`torch.FloatTensor` of shape `(num_clips, num_mel_bins, num_frames)`, *optional*):
|
| 1358 |
+
The log-mel spectrograms of the input clips.
|
| 1359 |
+
feature_attention_mask (`torch.Tensor` of shape `(num_clips, num_frames)`, *optional*):
|
| 1360 |
+
The mask of the valid spectrogram frames of each clip.
|
| 1361 |
+
audio_out_codes (`torch.LongTensor` of shape `(num_frames, num_codebooks)`, *optional*):
|
| 1362 |
+
The codes of the speech frames in the prompt.
|
| 1363 |
+
generation_config ([`GenerationConfig`], *optional*):
|
| 1364 |
+
`max_new_tokens`, `do_sample`, `temperature`, `top_k`, `top_p`, `min_p` and `repetition_penalty`
|
| 1365 |
+
apply to the text tokens; the penalty covers the last `text_repetition_window` generated tokens.
|
| 1366 |
+
Without `max_new_tokens`, an explicit `max_length` bounds the prompt plus the generated slots and
|
| 1367 |
+
must exceed the prompt; the default `max_length` counts generated slots alone, as in
|
| 1368 |
+
[`GenerationMixin`].
|
| 1369 |
+
Fields the text/speech mode machine does not implement (beam search, contrastive search,
|
| 1370 |
+
`stop_strings`, `output_*`, `use_cache=False`, cache implementations, ...) raise instead of being
|
| 1371 |
+
ignored. The bare call returns [`Lfm3OmniGenerateOutput`]. `return_dict_in_generate` is read from
|
| 1372 |
+
the call, then a passed configuration, then the model's `generation_config`: `False` returns the
|
| 1373 |
+
sequences alone, and so does a passed configuration when none of them sets it, like
|
| 1374 |
+
[`GenerationMixin`], as pipelines expect.
|
| 1375 |
+
generation_mode (`str` or `list[str]`, *optional*):
|
| 1376 |
+
`"text"`, `"audio"` or `"interleaved"`, or one mode per row; inferred per row when omitted.
|
| 1377 |
+
audio_temperature (`float`, *optional*):
|
| 1378 |
+
The sampling temperature of the speech codes; `None` picks the most likely code.
|
| 1379 |
+
audio_top_k (`int`, *optional*):
|
| 1380 |
+
The top-k filtering of the speech codes.
|
| 1381 |
+
audio_repetition_penalty (`float`, *optional*):
|
| 1382 |
+
The penalty on the codes each codebook emitted in the last `audio_repetition_window` frames.
|
| 1383 |
+
audio_repetition_window (`int`, *optional*, defaults to 64):
|
| 1384 |
+
The number of past frames the audio repetition penalty covers.
|
| 1385 |
+
text_repetition_window (`int`, *optional*, defaults to 32):
|
| 1386 |
+
The number of past tokens the text repetition penalty covers.
|
| 1387 |
+
suppress_mixed (`bool`, *optional*, defaults to `False`):
|
| 1388 |
+
Whether to forbid `<|mixed_start|>`.
|
| 1389 |
+
suppress_audio_out (`bool`, *optional*, defaults to `False`):
|
| 1390 |
+
Whether to forbid `<|audio_start|>`.
|
| 1391 |
+
return_audio_eos (`bool`, *optional*, defaults to `False`):
|
| 1392 |
+
Whether to keep the end-of-audio frames in `audio_codes` and their placeholders in `sequences`, so
|
| 1393 |
+
the output can be fed back as a prompt; they must be dropped before decoding with the codec.
|
| 1394 |
+
streamer ([`~generation.BaseStreamer`], *optional*):
|
| 1395 |
+
Receives the prompt and then every generated text token. For batches, pass one streamer per row.
|
| 1396 |
+
audio_streamer ([`~generation.BaseStreamer`], *optional*):
|
| 1397 |
+
Receives every generated speech frame, `(num_codebooks,)`, as soon as it is sampled. For batches,
|
| 1398 |
+
pass one streamer per row. An entry may be `None`.
|
| 1399 |
+
past_key_values (`Cache`, *optional*):
|
| 1400 |
+
Cache from a prefix forward pass or `prefill_audio`, updated in place to cover the prompt and every
|
| 1401 |
+
generated slot, end-of-audio frames included (`return_audio_eos=True` returns them); a batch whose rows
|
| 1402 |
+
finished at different steps holds masked slots and cannot be continued. Pass `attention_mask`
|
| 1403 |
+
covering both the cached prefix and `input_ids`. Positions are computed from that full mask unless
|
| 1404 |
+
supplied.
|
| 1405 |
+
"""
|
| 1406 |
+
# a consumer blocked on a streamer must see the end whichever statement raises, argument checks included
|
| 1407 |
+
streams = list(
|
| 1408 |
+
{
|
| 1409 |
+
id(stream): stream
|
| 1410 |
+
for value in (streamer, audio_streamer)
|
| 1411 |
+
for stream in (value if isinstance(value, (list, tuple)) else (value,))
|
| 1412 |
+
if stream is not None
|
| 1413 |
+
}.values()
|
| 1414 |
+
)
|
| 1415 |
+
try:
|
| 1416 |
+
output = self._generate(
|
| 1417 |
+
input_ids,
|
| 1418 |
+
input_features=input_features,
|
| 1419 |
+
feature_attention_mask=feature_attention_mask,
|
| 1420 |
+
audio_out_codes=audio_out_codes,
|
| 1421 |
+
generation_config=generation_config,
|
| 1422 |
+
generation_mode=generation_mode,
|
| 1423 |
+
audio_temperature=audio_temperature,
|
| 1424 |
+
audio_top_k=audio_top_k,
|
| 1425 |
+
audio_repetition_penalty=audio_repetition_penalty,
|
| 1426 |
+
audio_repetition_window=audio_repetition_window,
|
| 1427 |
+
text_repetition_window=text_repetition_window,
|
| 1428 |
+
suppress_mixed=suppress_mixed,
|
| 1429 |
+
suppress_audio_out=suppress_audio_out,
|
| 1430 |
+
return_audio_eos=return_audio_eos,
|
| 1431 |
+
streamer=streamer,
|
| 1432 |
+
audio_streamer=audio_streamer,
|
| 1433 |
+
past_key_values=past_key_values,
|
| 1434 |
+
**kwargs,
|
| 1435 |
+
)
|
| 1436 |
+
except BaseException:
|
| 1437 |
+
self._end_streamers(streams, raise_errors=False)
|
| 1438 |
+
raise
|
| 1439 |
+
self._end_streamers(streams)
|
| 1440 |
+
return output
|
| 1441 |
+
|
| 1442 |
+
@staticmethod
|
| 1443 |
+
def _end_streamers(streams: list, raise_errors: bool = True) -> None:
|
| 1444 |
+
# every consumer is released before an `end()` failure surfaces
|
| 1445 |
+
errors = []
|
| 1446 |
+
for stream in streams:
|
| 1447 |
+
try:
|
| 1448 |
+
stream.end()
|
| 1449 |
+
except Exception as error:
|
| 1450 |
+
errors.append(error)
|
| 1451 |
+
if errors and raise_errors:
|
| 1452 |
+
raise errors[0]
|
| 1453 |
+
|
| 1454 |
+
def _generate(
|
| 1455 |
+
self,
|
| 1456 |
+
input_ids: torch.LongTensor,
|
| 1457 |
+
input_features: torch.FloatTensor | None = None,
|
| 1458 |
+
feature_attention_mask: torch.Tensor | None = None,
|
| 1459 |
+
audio_out_codes: torch.LongTensor | None = None,
|
| 1460 |
+
generation_config: GenerationConfig | None = None,
|
| 1461 |
+
generation_mode: Lfm3OmniGenerationMode | str | list[str] | None = None,
|
| 1462 |
+
audio_temperature: float | None = None,
|
| 1463 |
+
audio_top_k: int | None = None,
|
| 1464 |
+
audio_repetition_penalty: float | None = None,
|
| 1465 |
+
audio_repetition_window: int = 64,
|
| 1466 |
+
text_repetition_window: int = 32,
|
| 1467 |
+
suppress_mixed: bool = False,
|
| 1468 |
+
suppress_audio_out: bool = False,
|
| 1469 |
+
return_audio_eos: bool = False,
|
| 1470 |
+
streamer: BaseStreamer | list[BaseStreamer | None] | None = None,
|
| 1471 |
+
audio_streamer: BaseStreamer | list[BaseStreamer | None] | None = None,
|
| 1472 |
+
past_key_values: Cache | None = None,
|
| 1473 |
+
**kwargs,
|
| 1474 |
+
) -> Lfm3OmniGenerateOutput | torch.LongTensor:
|
| 1475 |
+
if input_ids.ndim != 2 or not input_ids.shape[0] or not input_ids.shape[1]:
|
| 1476 |
+
raise ValueError("input_ids must be a nonempty batch of prompts")
|
| 1477 |
+
batch_size = input_ids.shape[0]
|
| 1478 |
+
for name in (
|
| 1479 |
+
"logits_processor",
|
| 1480 |
+
"stopping_criteria",
|
| 1481 |
+
"prefix_allowed_tokens_fn",
|
| 1482 |
+
"assistant_model",
|
| 1483 |
+
"negative_prompt_ids",
|
| 1484 |
+
"negative_prompt_attention_mask",
|
| 1485 |
+
"custom_generate",
|
| 1486 |
+
):
|
| 1487 |
+
if kwargs.get(name) is not None:
|
| 1488 |
+
raise ValueError(f"{type(self).__name__}.generate does not support `{name}`")
|
| 1489 |
+
# rows stop independently, so ZeRO-3 / FSDP ranks would leave the collective forwards at different steps
|
| 1490 |
+
if kwargs.pop("synced_gpus", None):
|
| 1491 |
+
raise ValueError(f"{type(self).__name__}.generate does not support `synced_gpus=True`")
|
| 1492 |
+
# call over passed configuration over `self.generation_config`; only a bare call defaults to the output object
|
| 1493 |
+
return_dict = kwargs.get("return_dict_in_generate")
|
| 1494 |
+
if return_dict is None and generation_config is not None:
|
| 1495 |
+
return_dict = generation_config.return_dict_in_generate
|
| 1496 |
+
if return_dict is None:
|
| 1497 |
+
return_dict = self.generation_config.return_dict_in_generate
|
| 1498 |
+
plain_sequences = return_dict is False or (return_dict is None and generation_config is not None)
|
| 1499 |
+
has_default_max_length = (
|
| 1500 |
+
kwargs.get("max_length") is None
|
| 1501 |
+
and (generation_config is None or generation_config.max_length is None)
|
| 1502 |
+
and self.generation_config.max_length is None
|
| 1503 |
+
)
|
| 1504 |
+
generation_config, model_kwargs = self._prepare_generation_config(generation_config, **kwargs)
|
| 1505 |
+
unsupported = sorted(
|
| 1506 |
+
name
|
| 1507 |
+
for name, inert in self._generation_fields_unsupported.items()
|
| 1508 |
+
if getattr(generation_config, name, inert) not in (inert, None)
|
| 1509 |
+
)
|
| 1510 |
+
if unsupported:
|
| 1511 |
+
raise ValueError(
|
| 1512 |
+
f"{type(self).__name__}.generate does not implement {unsupported}; see its docstring for the "
|
| 1513 |
+
"supported fields"
|
| 1514 |
+
)
|
| 1515 |
+
attention_mask = model_kwargs.pop("attention_mask", None)
|
| 1516 |
+
# only a caller that keeps the cache pays the decode step of the last sampled slot
|
| 1517 |
+
caller_cache = past_key_values is not None
|
| 1518 |
+
past_length = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 1519 |
+
if attention_mask is None and past_length:
|
| 1520 |
+
raise ValueError("Cached generation requires attention_mask covering the cached prefix and input_ids")
|
| 1521 |
+
if attention_mask is None:
|
| 1522 |
+
pad_id = generation_config.pad_token_id
|
| 1523 |
+
if pad_id is None:
|
| 1524 |
+
pad_id = self.config.text_config.pad_token_id
|
| 1525 |
+
eos_id = generation_config.eos_token_id
|
| 1526 |
+
if eos_id is None:
|
| 1527 |
+
eos_id = self.config.text_config.eos_token_id
|
| 1528 |
+
eos_ids = list(eos_id) if isinstance(eos_id, (list, tuple)) else [eos_id]
|
| 1529 |
+
attention_mask = (
|
| 1530 |
+
input_ids.ne(pad_id).long()
|
| 1531 |
+
if pad_id is not None and pad_id not in eos_ids
|
| 1532 |
+
else torch.ones_like(input_ids)
|
| 1533 |
+
)
|
| 1534 |
+
position_ids = model_kwargs.pop("position_ids", None)
|
| 1535 |
+
self._validate_model_kwargs(model_kwargs.copy())
|
| 1536 |
+
if generation_config.num_beams != 1 or generation_config.num_return_sequences != 1:
|
| 1537 |
+
raise ValueError(f"{type(self).__name__} supports one greedy or sampled continuation per input row")
|
| 1538 |
+
if attention_mask.shape != (batch_size, past_length + input_ids.shape[1]) or not bool(
|
| 1539 |
+
attention_mask[:, -1].all()
|
| 1540 |
+
):
|
| 1541 |
+
raise ValueError("Use left-padded prompts and an attention_mask covering the cached prefix and input_ids")
|
| 1542 |
+
not_binary = ((attention_mask != 0) & (attention_mask != 1)).any()
|
| 1543 |
+
not_monotonic = ((attention_mask[:, 1:].long() - attention_mask[:, :-1].long()) < 0).any()
|
| 1544 |
+
if bool(not_binary | not_monotonic):
|
| 1545 |
+
raise ValueError("attention_mask must describe contiguous left-padded prompts of zeros and ones")
|
| 1546 |
+
if position_ids is None:
|
| 1547 |
+
position_ids = attention_mask.long().cumsum(-1).sub(1).clamp_min(0)[:, -input_ids.shape[1] :]
|
| 1548 |
+
if position_ids.shape not in ((1, input_ids.shape[1]), input_ids.shape):
|
| 1549 |
+
raise ValueError("position_ids must cover the uncached input_ids, with one row or one row per prompt")
|
| 1550 |
+
if audio_repetition_window < 0 or text_repetition_window < 0:
|
| 1551 |
+
raise ValueError("Repetition windows must be nonnegative")
|
| 1552 |
+
text_temperature = generation_config.temperature if generation_config.do_sample else None
|
| 1553 |
+
text_top_k = generation_config.top_k if generation_config.do_sample else None
|
| 1554 |
+
text_top_p = generation_config.top_p if generation_config.do_sample else None
|
| 1555 |
+
text_min_p = generation_config.min_p if generation_config.do_sample else None
|
| 1556 |
+
config = self.config
|
| 1557 |
+
pad_token_id = generation_config.pad_token_id
|
| 1558 |
+
if pad_token_id is None:
|
| 1559 |
+
pad_token_id = config.text_config.pad_token_id
|
| 1560 |
+
if pad_token_id is None:
|
| 1561 |
+
pad_token_id = config.text_config.eos_token_id
|
| 1562 |
+
if isinstance(pad_token_id, (list, tuple)):
|
| 1563 |
+
pad_token_id = pad_token_id[0]
|
| 1564 |
+
if pad_token_id is None:
|
| 1565 |
+
raise ValueError("Set pad_token_id to assemble generated sequences")
|
| 1566 |
+
eos_ids = generation_config.eos_token_id
|
| 1567 |
+
if eos_ids is None:
|
| 1568 |
+
eos_ids = config.text_config.eos_token_id
|
| 1569 |
+
eos_ids = list(eos_ids) if isinstance(eos_ids, (list, tuple)) else [eos_ids]
|
| 1570 |
+
suppress = self._placeholder_token_ids + [
|
| 1571 |
+
token_id
|
| 1572 |
+
for flag, token_id in (
|
| 1573 |
+
(suppress_mixed, config.mixed_start_token_id),
|
| 1574 |
+
(suppress_audio_out, config.audio_start_token_id),
|
| 1575 |
+
)
|
| 1576 |
+
if flag
|
| 1577 |
+
]
|
| 1578 |
+
if generation_mode is None:
|
| 1579 |
+
modes = [
|
| 1580 |
+
Lfm3OmniGenerationMode.AUDIO
|
| 1581 |
+
if token in (config.audio_start_token_id, config.audio_out_token_id)
|
| 1582 |
+
else Lfm3OmniGenerationMode.INTERLEAVED
|
| 1583 |
+
if token == config.mixed_start_token_id
|
| 1584 |
+
else Lfm3OmniGenerationMode.TEXT
|
| 1585 |
+
for token in input_ids[:, -1].tolist()
|
| 1586 |
+
]
|
| 1587 |
+
else:
|
| 1588 |
+
modes = generation_mode if isinstance(generation_mode, list) else [generation_mode] * batch_size
|
| 1589 |
+
if len(modes) != batch_size:
|
| 1590 |
+
raise ValueError("generation_mode must have one entry per input row")
|
| 1591 |
+
modes = [Lfm3OmniGenerationMode(mode) for mode in modes]
|
| 1592 |
+
# speech always follows its marker in training; append the one the requested mode needs
|
| 1593 |
+
markers = {
|
| 1594 |
+
Lfm3OmniGenerationMode.AUDIO: (config.audio_start_token_id, config.audio_out_token_id),
|
| 1595 |
+
Lfm3OmniGenerationMode.INTERLEAVED: (config.mixed_start_token_id,),
|
| 1596 |
+
}
|
| 1597 |
+
needed = [
|
| 1598 |
+
markers[mode][0] if mode in markers and token not in markers[mode] else None
|
| 1599 |
+
for mode, token in zip(modes, input_ids[:, -1].tolist())
|
| 1600 |
+
]
|
| 1601 |
+
if len(set(needed)) > 1:
|
| 1602 |
+
raise ValueError("Rows need different generation markers; end each prompt with its marker instead")
|
| 1603 |
+
if needed[0] is not None:
|
| 1604 |
+
marker = input_ids.new_full((batch_size, 1), needed[0])
|
| 1605 |
+
input_ids = torch.cat((input_ids, marker), 1)
|
| 1606 |
+
attention_mask = torch.cat((attention_mask, attention_mask.new_ones((batch_size, 1))), 1)
|
| 1607 |
+
position_ids = torch.cat((position_ids, position_ids[:, -1:] + 1), 1)
|
| 1608 |
+
|
| 1609 |
+
def row_streamers(value):
|
| 1610 |
+
if value is None:
|
| 1611 |
+
return [None] * batch_size
|
| 1612 |
+
if isinstance(value, (list, tuple)) and len(value) == batch_size:
|
| 1613 |
+
return list(value)
|
| 1614 |
+
if batch_size == 1 and not isinstance(value, (list, tuple)):
|
| 1615 |
+
return [value]
|
| 1616 |
+
raise ValueError("Pass one streamer per input row")
|
| 1617 |
+
|
| 1618 |
+
text_streams, audio_streams = row_streamers(streamer), row_streamers(audio_streamer)
|
| 1619 |
+
# the appended marker is part of the prompt, so `max_length` counts it
|
| 1620 |
+
max_new_tokens = generation_config.max_new_tokens
|
| 1621 |
+
if max_new_tokens is None and has_default_max_length:
|
| 1622 |
+
warnings.warn(
|
| 1623 |
+
f"Using the model-agnostic default `max_length` (={generation_config.max_length}) to control the "
|
| 1624 |
+
"generation length. We recommend setting `max_new_tokens` to control the maximum length of the "
|
| 1625 |
+
"generation.",
|
| 1626 |
+
UserWarning,
|
| 1627 |
+
)
|
| 1628 |
+
max_new_tokens = generation_config.max_length
|
| 1629 |
+
elif max_new_tokens is None:
|
| 1630 |
+
prompt_length = past_length + input_ids.shape[1]
|
| 1631 |
+
if prompt_length >= generation_config.max_length:
|
| 1632 |
+
raise ValueError(
|
| 1633 |
+
f"Input length of input_ids is {prompt_length}, but `max_length` is set to "
|
| 1634 |
+
f"{generation_config.max_length}. Increase `max_length` or set `max_new_tokens`."
|
| 1635 |
+
)
|
| 1636 |
+
max_new_tokens = generation_config.max_length - prompt_length
|
| 1637 |
+
# the warper constructors validate the sampling values: build them before the prefill mutates a passed
|
| 1638 |
+
# cache
|
| 1639 |
+
text_warpers = self._build_warpers(text_temperature, text_top_k, text_top_p, text_min_p)
|
| 1640 |
+
audio_warpers = self._build_warpers(audio_temperature, audio_top_k)
|
| 1641 |
+
for penalty in (generation_config.repetition_penalty, audio_repetition_penalty):
|
| 1642 |
+
if penalty is not None and penalty != 1.0:
|
| 1643 |
+
RepetitionPenaltyLogitsProcessor(float(penalty))
|
| 1644 |
+
outputs = self.model(
|
| 1645 |
+
input_ids=input_ids,
|
| 1646 |
+
attention_mask=attention_mask,
|
| 1647 |
+
position_ids=position_ids,
|
| 1648 |
+
input_features=input_features,
|
| 1649 |
+
feature_attention_mask=feature_attention_mask,
|
| 1650 |
+
audio_out_codes=audio_out_codes,
|
| 1651 |
+
use_cache=True,
|
| 1652 |
+
past_key_values=past_key_values
|
| 1653 |
+
if past_key_values is not None
|
| 1654 |
+
else DynamicCache(config=config.text_config),
|
| 1655 |
+
**model_kwargs,
|
| 1656 |
+
)
|
| 1657 |
+
# Prefill has validated the flattened frame/placeholder mapping. A replayed terminal frame resumes
|
| 1658 |
+
# the text phase, just as it does when sampled inside this call; explicit mode overrides still win.
|
| 1659 |
+
if generation_mode is None and audio_out_codes is not None:
|
| 1660 |
+
last_frames = (input_ids == config.audio_out_token_id).sum(-1).cumsum(0) - 1
|
| 1661 |
+
for row, last_frame in enumerate(last_frames.tolist()):
|
| 1662 |
+
if input_ids[row, -1] == config.audio_out_token_id:
|
| 1663 |
+
if bool((audio_out_codes[last_frame] == config.audio_eos_code).any()):
|
| 1664 |
+
modes[row] = Lfm3OmniGenerationMode.TEXT
|
| 1665 |
+
hidden, cache = outputs.last_hidden_state[:, -1], outputs.past_key_values
|
| 1666 |
+
embed_tokens = self.get_input_embeddings()
|
| 1667 |
+
for row, stream in enumerate(text_streams):
|
| 1668 |
+
if stream is not None:
|
| 1669 |
+
stream.put(input_ids[row : row + 1])
|
| 1670 |
+
text_out = [[] for _ in range(batch_size)]
|
| 1671 |
+
frames = [[] for _ in range(batch_size)]
|
| 1672 |
+
text_history = [deque(maxlen=text_repetition_window) for _ in range(batch_size)]
|
| 1673 |
+
audio_history = [deque(maxlen=audio_repetition_window) for _ in range(batch_size)]
|
| 1674 |
+
slots_left = [config.interleaved_n_text] * batch_size
|
| 1675 |
+
speaking, text_done, finished = [False] * batch_size, [False] * batch_size, [False] * batch_size
|
| 1676 |
+
for step in range(max_new_tokens):
|
| 1677 |
+
interleaving = [mode is Lfm3OmniGenerationMode.INTERLEAVED for mode in modes]
|
| 1678 |
+
audio_rows = [
|
| 1679 |
+
row
|
| 1680 |
+
for row in range(batch_size)
|
| 1681 |
+
if not finished[row]
|
| 1682 |
+
and (modes[row] is Lfm3OmniGenerationMode.AUDIO or (interleaving[row] and speaking[row]))
|
| 1683 |
+
]
|
| 1684 |
+
sampled_frames = {}
|
| 1685 |
+
if audio_rows:
|
| 1686 |
+
histories = [
|
| 1687 |
+
torch.stack(tuple(audio_history[row])) if audio_history[row] else None for row in audio_rows
|
| 1688 |
+
]
|
| 1689 |
+
if len(audio_rows) == 1:
|
| 1690 |
+
sampled = self.sample_audio_frame(
|
| 1691 |
+
hidden[audio_rows[0]],
|
| 1692 |
+
audio_temperature,
|
| 1693 |
+
audio_top_k,
|
| 1694 |
+
audio_repetition_penalty,
|
| 1695 |
+
histories[0],
|
| 1696 |
+
audio_warpers,
|
| 1697 |
+
)[None]
|
| 1698 |
+
else:
|
| 1699 |
+
sampled = self.sample_audio_frame(
|
| 1700 |
+
hidden[audio_rows],
|
| 1701 |
+
audio_temperature,
|
| 1702 |
+
audio_top_k,
|
| 1703 |
+
audio_repetition_penalty,
|
| 1704 |
+
histories,
|
| 1705 |
+
audio_warpers,
|
| 1706 |
+
)
|
| 1707 |
+
sampled_frames = dict(zip(audio_rows, sampled))
|
| 1708 |
+
# the rows still running feed this step's slot, so a caller's cache covers every returned slot
|
| 1709 |
+
active = input_ids.new_tensor([not done for done in finished])[:, None]
|
| 1710 |
+
next_embeds = hidden.new_zeros((batch_size, 1, hidden.shape[-1]))
|
| 1711 |
+
for row in range(batch_size):
|
| 1712 |
+
if finished[row]:
|
| 1713 |
+
continue
|
| 1714 |
+
if row in sampled_frames:
|
| 1715 |
+
codes = sampled_frames[row]
|
| 1716 |
+
is_eos = bool((codes == config.audio_eos_code).any())
|
| 1717 |
+
if is_eos:
|
| 1718 |
+
codes = torch.full_like(codes, config.audio_eos_code)
|
| 1719 |
+
modes[row] = Lfm3OmniGenerationMode.TEXT
|
| 1720 |
+
else:
|
| 1721 |
+
audio_history[row].append(codes)
|
| 1722 |
+
if audio_streams[row] is not None:
|
| 1723 |
+
audio_streams[row].put(codes)
|
| 1724 |
+
if not is_eos or return_audio_eos:
|
| 1725 |
+
frames[row].append(codes)
|
| 1726 |
+
text_out[row].append(config.audio_out_token_id)
|
| 1727 |
+
next_embeds[row] = self.model.get_audio_out_embeds(codes[None])
|
| 1728 |
+
else:
|
| 1729 |
+
history = torch.stack(tuple(text_history[row])) if text_history[row] else None
|
| 1730 |
+
token = self._sample(
|
| 1731 |
+
self.lm_head(hidden[row]),
|
| 1732 |
+
text_temperature,
|
| 1733 |
+
text_top_k,
|
| 1734 |
+
generation_config.repetition_penalty,
|
| 1735 |
+
history,
|
| 1736 |
+
suppress,
|
| 1737 |
+
top_p=text_top_p,
|
| 1738 |
+
min_p=text_min_p,
|
| 1739 |
+
warpers=text_warpers,
|
| 1740 |
+
)
|
| 1741 |
+
token_id = int(token)
|
| 1742 |
+
text_history[row].append(token)
|
| 1743 |
+
text_out[row].append(token_id)
|
| 1744 |
+
if text_streams[row] is not None:
|
| 1745 |
+
text_streams[row].put(token.view(1, 1))
|
| 1746 |
+
if token_id == config.text_end_token_id:
|
| 1747 |
+
# inside an interleaved block it ends the text and starts the audio tail; otherwise it ends
|
| 1748 |
+
# the turn, so listing it in `eos_token_id` for text-only serving changes nothing here
|
| 1749 |
+
if interleaving[row]:
|
| 1750 |
+
text_done[row], speaking[row] = True, True
|
| 1751 |
+
else:
|
| 1752 |
+
finished[row] = True
|
| 1753 |
+
elif token_id in eos_ids:
|
| 1754 |
+
# `<|im_end|>` ends the turn in any text slot, interleaved blocks included
|
| 1755 |
+
finished[row] = True
|
| 1756 |
+
elif interleaving[row]:
|
| 1757 |
+
pass # the mode markers only open a block; inside one they are ordinary text
|
| 1758 |
+
elif token_id == config.audio_start_token_id:
|
| 1759 |
+
modes[row] = Lfm3OmniGenerationMode.AUDIO
|
| 1760 |
+
elif token_id == config.mixed_start_token_id:
|
| 1761 |
+
modes[row] = Lfm3OmniGenerationMode.INTERLEAVED
|
| 1762 |
+
slots_left[row], speaking[row], text_done[row] = config.interleaved_n_text, False, False
|
| 1763 |
+
next_embeds[row] = embed_tokens(token.view(1))
|
| 1764 |
+
if interleaving[row] and not text_done[row]:
|
| 1765 |
+
slots_left[row] -= 1
|
| 1766 |
+
if slots_left[row] == 0:
|
| 1767 |
+
speaking[row] = not speaking[row]
|
| 1768 |
+
slots_left[row] = config.interleaved_n_audio if speaking[row] else config.interleaved_n_text
|
| 1769 |
+
last_step = all(finished) or step == max_new_tokens - 1
|
| 1770 |
+
if last_step and not caller_cache:
|
| 1771 |
+
# the last sampled token needs no hidden state: skip the decode step nothing would consume
|
| 1772 |
+
break
|
| 1773 |
+
attention_mask = torch.cat((attention_mask, active), dim=1)
|
| 1774 |
+
position_ids = position_ids[:, -1:] + active
|
| 1775 |
+
outputs = self.model.language_model(
|
| 1776 |
+
inputs_embeds=next_embeds,
|
| 1777 |
+
attention_mask=attention_mask,
|
| 1778 |
+
position_ids=position_ids,
|
| 1779 |
+
past_key_values=cache,
|
| 1780 |
+
use_cache=True,
|
| 1781 |
+
)
|
| 1782 |
+
if last_step:
|
| 1783 |
+
break
|
| 1784 |
+
hidden, cache = outputs.last_hidden_state[:, -1], outputs.past_key_values
|
| 1785 |
+
generated = input_ids.new_full((batch_size, max(map(len, text_out))), pad_token_id)
|
| 1786 |
+
audio_codes = input_ids.new_full((batch_size, config.num_codebooks, max(map(len, frames))), -1)
|
| 1787 |
+
for row in range(batch_size):
|
| 1788 |
+
generated[row, : len(text_out[row])] = input_ids.new_tensor(text_out[row])
|
| 1789 |
+
if frames[row]:
|
| 1790 |
+
audio_codes[row, :, : len(frames[row])] = torch.stack(frames[row], dim=-1)
|
| 1791 |
+
output = Lfm3OmniGenerateOutput(
|
| 1792 |
+
sequences=torch.cat((input_ids, generated), dim=1),
|
| 1793 |
+
audio_codes=audio_codes,
|
| 1794 |
+
sequence_lengths=input_ids.new_tensor([input_ids.shape[1] + len(tokens) for tokens in text_out]),
|
| 1795 |
+
audio_lengths=input_ids.new_tensor([len(row_frames) for row_frames in frames]),
|
| 1796 |
+
)
|
| 1797 |
+
return output.sequences if plain_sequences else output
|
| 1798 |
+
|
| 1799 |
+
@auto_docstring
|
| 1800 |
+
def get_image_features(self, pixel_values: torch.Tensor, image_grid_thw: torch.LongTensor):
|
| 1801 |
+
r"""
|
| 1802 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 1803 |
+
Flattened image patches. `patch_dim` is channels times temporal patch size times squared spatial patch size.
|
| 1804 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 1805 |
+
Temporal, height, and width patch counts before spatial merging, in image placeholder order.
|
| 1806 |
+
|
| 1807 |
+
Returns:
|
| 1808 |
+
`torch.Tensor` of shape `(num_image_tokens, hidden_size)`: Projected image features in placeholder order.
|
| 1809 |
+
"""
|
| 1810 |
+
return self.model.get_image_features(pixel_values, image_grid_thw)
|
| 1811 |
+
|
| 1812 |
+
@auto_docstring
|
| 1813 |
+
def get_video_features(self, pixel_values_videos: torch.Tensor, video_grid_thw: torch.LongTensor):
|
| 1814 |
+
r"""
|
| 1815 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, patch_dim)`, *optional*):
|
| 1816 |
+
Flattened video patches; the tower encodes every temporal patch as its own spatial sequence.
|
| 1817 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
|
| 1818 |
+
Temporal, height, and width patch counts before spatial merging, in video placeholder order.
|
| 1819 |
+
|
| 1820 |
+
Returns:
|
| 1821 |
+
`torch.Tensor` of shape `(num_video_tokens, hidden_size)`: Projected video features in placeholder order.
|
| 1822 |
+
"""
|
| 1823 |
+
return self.model.get_video_features(pixel_values_videos, video_grid_thw)
|
| 1824 |
+
|
| 1825 |
+
|
| 1826 |
+
@dataclass
|
| 1827 |
+
class Lfm3OmniFeatureStreamingCache:
|
| 1828 |
+
"""Unconsumed mono samples and frame count for one input stream."""
|
| 1829 |
+
|
| 1830 |
+
samples: torch.Tensor | None = None
|
| 1831 |
+
num_samples: int = 0
|
| 1832 |
+
num_frames: int = 0
|
| 1833 |
+
finished: bool = False
|
| 1834 |
+
|
| 1835 |
+
|
| 1836 |
+
class Lfm3OmniFeatureExtractorKwargs(TypedDict, total=False):
|
| 1837 |
+
"""
|
| 1838 |
+
sampling_rate (`int`, *optional*):
|
| 1839 |
+
The sampling rate of the clips, which must be the extractor's own; it is checked, never resampled.
|
| 1840 |
+
return_tensors (`str` or [`~utils.TensorType`], *optional*):
|
| 1841 |
+
The tensor type of the returned features.
|
| 1842 |
+
load_audio_backend (`str`, *optional*):
|
| 1843 |
+
The decoding backend `apply_chat_template` loads audio with; the extractor itself ignores it.
|
| 1844 |
+
"""
|
| 1845 |
+
|
| 1846 |
+
# the extractor always right-pads to whole frames and returns the mask, so the padding, truncation and
|
| 1847 |
+
# attention-mask options of `AudioKwargs` are not offered rather than silently dropped
|
| 1848 |
+
sampling_rate: int | None
|
| 1849 |
+
return_tensors: str | TensorType | None
|
| 1850 |
+
load_audio_backend: str | None
|
| 1851 |
+
|
| 1852 |
+
|
| 1853 |
+
__all__ = [
|
| 1854 |
+
"Lfm3OmniFeatureStreamingCache",
|
| 1855 |
+
"Lfm3OmniEncoderStreamingCache",
|
| 1856 |
+
"Lfm3OmniLiveTranscriptionState",
|
| 1857 |
+
"Lfm3OmniLiveTranscriptionOutput",
|
| 1858 |
+
"Lfm3OmniModel",
|
| 1859 |
+
"Lfm3OmniPreTrainedModel",
|
| 1860 |
+
"Lfm3OmniForConditionalGeneration",
|
| 1861 |
+
"Lfm3OmniGenerateOutput",
|
| 1862 |
+
"Lfm3OmniStreamingOutput",
|
| 1863 |
+
]
|
modeling_lfm3_vl.py
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
from collections.abc import Callable
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from torch import nn
|
| 20 |
+
|
| 21 |
+
from transformers.activations import ACT2FN
|
| 22 |
+
from transformers.cache_utils import Cache
|
| 23 |
+
from transformers.configuration_utils import PreTrainedConfig
|
| 24 |
+
from transformers.generation import GenerationMixin
|
| 25 |
+
from transformers.integrations import use_kernel_forward_from_hub
|
| 26 |
+
from transformers.masking_utils import (
|
| 27 |
+
create_causal_mask,
|
| 28 |
+
create_recurrent_attention_mask,
|
| 29 |
+
create_sliding_window_causal_mask,
|
| 30 |
+
)
|
| 31 |
+
from transformers.modeling_layers import GradientCheckpointingLayer
|
| 32 |
+
from transformers.modeling_outputs import BaseModelOutputWithPast, BaseModelOutputWithPooling, CausalLMOutputWithPast
|
| 33 |
+
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
|
| 34 |
+
from transformers.processing_utils import Unpack
|
| 35 |
+
from transformers.utils import (
|
| 36 |
+
TransformersKwargs,
|
| 37 |
+
auto_docstring,
|
| 38 |
+
can_return_tuple,
|
| 39 |
+
torch_compilable_check,
|
| 40 |
+
)
|
| 41 |
+
from transformers.utils.generic import (
|
| 42 |
+
accepts_precomputed_kwargs,
|
| 43 |
+
is_flash_attention_requested,
|
| 44 |
+
maybe_autocast,
|
| 45 |
+
merge_with_config_defaults,
|
| 46 |
+
)
|
| 47 |
+
from transformers.utils.output_capturing import OutputRecorder, capture_outputs
|
| 48 |
+
from transformers.vision_utils import get_max_seqlen, get_vision_attention_seqlens, get_vision_position_ids
|
| 49 |
+
|
| 50 |
+
from .configuration_lfm3_vl import Lfm3VlConfig, Lfm3VlVisionConfig
|
| 51 |
+
from .modeling_lfm3 import Lfm3Model
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@use_kernel_forward_from_hub("RMSNorm")
|
| 55 |
+
class Lfm3VlRMSNorm(nn.Module):
|
| 56 |
+
def __init__(self, hidden_size, eps: float = 1e-6) -> None:
|
| 57 |
+
"""
|
| 58 |
+
Lfm3VlRMSNorm is equivalent to T5LayerNorm
|
| 59 |
+
"""
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 62 |
+
self.variance_epsilon = eps
|
| 63 |
+
|
| 64 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 65 |
+
input_dtype = hidden_states.dtype
|
| 66 |
+
hidden_states = hidden_states.to(torch.float32)
|
| 67 |
+
variance = hidden_states.pow(2).mean(-1, keepdim=True)
|
| 68 |
+
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
|
| 69 |
+
return self.weight * hidden_states.to(input_dtype)
|
| 70 |
+
|
| 71 |
+
def extra_repr(self):
|
| 72 |
+
return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class Lfm3VlMLP(nn.Module):
|
| 76 |
+
def __init__(self, config: Lfm3VlConfig):
|
| 77 |
+
super().__init__()
|
| 78 |
+
self.w1 = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
| 79 |
+
self.w3 = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
|
| 80 |
+
self.w2 = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
|
| 81 |
+
|
| 82 |
+
def forward(self, x):
|
| 83 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class Lfm3VlVisionRotaryEmbedding(nn.Module):
|
| 87 |
+
"""
|
| 88 |
+
Simple axial 2D rope with same freqs used for H and W grids. The freqs are
|
| 89 |
+
pre-computed using `head-dim//4` which is later used to concat H and W positions.
|
| 90 |
+
The final angles rotate over the whole head dim, no partial rotation involved.
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def __init__(self, config: Lfm3VlVisionConfig):
|
| 94 |
+
super().__init__()
|
| 95 |
+
self.config = config
|
| 96 |
+
|
| 97 |
+
self.rope_type = self.config.rope_parameters["rope_type"]
|
| 98 |
+
rope_init_fn: Callable = self.compute_axial_rope_parameters
|
| 99 |
+
if self.rope_type != "axial":
|
| 100 |
+
raise ValueError(f"{self.__class__.__name__} supports only axial rope, but requested {self.rope_type}")
|
| 101 |
+
inv_freq, self.attention_scaling = rope_init_fn(self.config)
|
| 102 |
+
|
| 103 |
+
self.inv_freq = nn.Buffer(inv_freq, persistent=False)
|
| 104 |
+
self.original_inv_freq = nn.Buffer(inv_freq.clone(), persistent=False)
|
| 105 |
+
|
| 106 |
+
@staticmethod
|
| 107 |
+
def compute_axial_rope_parameters(config: Lfm3VlVisionConfig, **kwargs) -> tuple[torch.Tensor, float]:
|
| 108 |
+
"""Inverse frequencies of the axial RoPE over half the head dimension per axis, with an attention scaling of 1."""
|
| 109 |
+
base = config.rope_parameters["rope_theta"]
|
| 110 |
+
spatial_dim = config.hidden_size // config.num_attention_heads // 2
|
| 111 |
+
inv_freq = 1.0 / (base ** (torch.arange(0, spatial_dim, 2, dtype=torch.float) / spatial_dim))
|
| 112 |
+
return inv_freq, 1.0
|
| 113 |
+
|
| 114 |
+
@torch.no_grad()
|
| 115 |
+
def forward(self, x, position_ids):
|
| 116 |
+
# position_ids: (2, N) — row 0 = h coords, row 1 = w coords
|
| 117 |
+
position_ids_expanded = position_ids[..., None].float()
|
| 118 |
+
device_type = x.device.type if isinstance(x.device.type, str) else "cpu"
|
| 119 |
+
with maybe_autocast(device_type=device_type, enabled=False):
|
| 120 |
+
freqs = position_ids_expanded * self.inv_freq.float()
|
| 121 |
+
cos = freqs.cos() * self.attention_scaling
|
| 122 |
+
sin = freqs.sin() * self.attention_scaling
|
| 123 |
+
|
| 124 |
+
cos = self.recomposition_frequencies(cos)
|
| 125 |
+
sin = self.recomposition_frequencies(sin)
|
| 126 |
+
return cos, sin
|
| 127 |
+
|
| 128 |
+
def recomposition_frequencies(self, freq):
|
| 129 |
+
"""
|
| 130 |
+
Recompose the frequencies into the final spatial layout used per each grid.
|
| 131 |
+
"""
|
| 132 |
+
freq_h, freq_w = freq[:, 0], freq[:, 1]
|
| 133 |
+
freq_hw = torch.cat([freq_h, freq_w], dim=-1)
|
| 134 |
+
return torch.cat([freq_hw, freq_hw], dim=-1)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def rotate_half(x):
|
| 138 |
+
"""Rotates half the hidden dims of the input."""
|
| 139 |
+
x1 = x[..., : x.shape[-1] // 2]
|
| 140 |
+
x2 = x[..., x.shape[-1] // 2 :]
|
| 141 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def apply_rotary_pos_emb_vision(
|
| 145 |
+
q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
|
| 146 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 147 |
+
orig_q_dtype = q.dtype
|
| 148 |
+
orig_k_dtype = k.dtype
|
| 149 |
+
q, k = q.float(), k.float()
|
| 150 |
+
cos, sin = cos.unsqueeze(-2).float(), sin.unsqueeze(-2).float()
|
| 151 |
+
q_embed = (q * cos) + (rotate_half(q) * sin)
|
| 152 |
+
k_embed = (k * cos) + (rotate_half(k) * sin)
|
| 153 |
+
q_embed = q_embed.to(orig_q_dtype)
|
| 154 |
+
k_embed = k_embed.to(orig_k_dtype)
|
| 155 |
+
return q_embed, k_embed
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
|
| 159 |
+
"""
|
| 160 |
+
This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
|
| 161 |
+
num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
|
| 162 |
+
"""
|
| 163 |
+
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
|
| 164 |
+
if n_rep == 1:
|
| 165 |
+
return hidden_states
|
| 166 |
+
hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
|
| 167 |
+
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def eager_attention_forward(
|
| 171 |
+
module: nn.Module,
|
| 172 |
+
query: torch.Tensor,
|
| 173 |
+
key: torch.Tensor,
|
| 174 |
+
value: torch.Tensor,
|
| 175 |
+
attention_mask: torch.Tensor | None,
|
| 176 |
+
scaling: float,
|
| 177 |
+
dropout: float = 0.0,
|
| 178 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 179 |
+
):
|
| 180 |
+
key_states = repeat_kv(key, module.num_key_value_groups)
|
| 181 |
+
value_states = repeat_kv(value, module.num_key_value_groups)
|
| 182 |
+
|
| 183 |
+
attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
|
| 184 |
+
if attention_mask is not None:
|
| 185 |
+
attn_weights = attn_weights + attention_mask
|
| 186 |
+
|
| 187 |
+
attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
|
| 188 |
+
attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
|
| 189 |
+
attn_output = torch.matmul(attn_weights, value_states)
|
| 190 |
+
attn_output = attn_output.transpose(1, 2).contiguous()
|
| 191 |
+
|
| 192 |
+
return attn_output, attn_weights
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class Lfm3VlVisionAttention(nn.Module):
|
| 196 |
+
"""Bidirectional grouped-query attention over the patches of one frame, with RMSNorm on the query and key heads."""
|
| 197 |
+
|
| 198 |
+
def __init__(self, config: Lfm3VlVisionConfig):
|
| 199 |
+
super().__init__()
|
| 200 |
+
self.config = config
|
| 201 |
+
self.num_heads = config.num_attention_heads
|
| 202 |
+
self.num_key_value_heads = config.num_key_value_heads
|
| 203 |
+
self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads
|
| 204 |
+
self.head_dim = config.hidden_size // config.num_attention_heads
|
| 205 |
+
self.scaling = self.head_dim**-0.5
|
| 206 |
+
self.attention_dropout = 0.0
|
| 207 |
+
self.is_causal = False
|
| 208 |
+
self.q_proj = nn.Linear(config.hidden_size, self.num_heads * self.head_dim, bias=False)
|
| 209 |
+
self.k_proj = nn.Linear(config.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)
|
| 210 |
+
self.v_proj = nn.Linear(config.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)
|
| 211 |
+
self.out_proj = nn.Linear(self.num_heads * self.head_dim, config.hidden_size, bias=False)
|
| 212 |
+
self.q_layernorm = Lfm3VlRMSNorm(self.head_dim, eps=config.norm_eps)
|
| 213 |
+
self.k_layernorm = Lfm3VlRMSNorm(self.head_dim, eps=config.norm_eps)
|
| 214 |
+
|
| 215 |
+
def forward(
|
| 216 |
+
self,
|
| 217 |
+
hidden_states: torch.Tensor,
|
| 218 |
+
cu_seqlens: torch.Tensor,
|
| 219 |
+
position_embeddings: tuple[torch.Tensor, torch.Tensor],
|
| 220 |
+
max_seqlen: int | None = None,
|
| 221 |
+
frame_shapes: list[list[int]] | None = None,
|
| 222 |
+
**kwargs,
|
| 223 |
+
) -> torch.Tensor:
|
| 224 |
+
seq_length = hidden_states.shape[0]
|
| 225 |
+
query_states = self.q_layernorm(self.q_proj(hidden_states).view(seq_length, self.num_heads, self.head_dim))
|
| 226 |
+
key_states = self.k_layernorm(
|
| 227 |
+
self.k_proj(hidden_states).view(seq_length, self.num_key_value_heads, self.head_dim)
|
| 228 |
+
)
|
| 229 |
+
value_states = self.v_proj(hidden_states).view(seq_length, self.num_key_value_heads, self.head_dim)
|
| 230 |
+
|
| 231 |
+
cos, sin = position_embeddings
|
| 232 |
+
query_states, key_states = apply_rotary_pos_emb_vision(query_states, key_states, cos, sin)
|
| 233 |
+
|
| 234 |
+
query_states = query_states.transpose(0, 1).unsqueeze(0)
|
| 235 |
+
key_states = key_states.transpose(0, 1).unsqueeze(0)
|
| 236 |
+
value_states = value_states.transpose(0, 1).unsqueeze(0)
|
| 237 |
+
|
| 238 |
+
attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface(
|
| 239 |
+
self.config._attn_implementation, eager_attention_forward
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
if is_flash_attention_requested(self.config):
|
| 243 |
+
max_seqlen = get_max_seqlen(cu_seqlens, self.config, kwargs={"max_seqlen": max_seqlen})
|
| 244 |
+
attn_output, attn_weights = attention_interface(
|
| 245 |
+
self,
|
| 246 |
+
query_states,
|
| 247 |
+
key_states,
|
| 248 |
+
value_states,
|
| 249 |
+
attention_mask=None,
|
| 250 |
+
scaling=self.scaling,
|
| 251 |
+
dropout=0.0,
|
| 252 |
+
cu_seq_lens_q=cu_seqlens,
|
| 253 |
+
cu_seq_lens_k=cu_seqlens,
|
| 254 |
+
max_length_q=max_seqlen,
|
| 255 |
+
max_length_k=max_seqlen,
|
| 256 |
+
is_causal=False,
|
| 257 |
+
**kwargs,
|
| 258 |
+
)
|
| 259 |
+
else:
|
| 260 |
+
# Other implementations: frames of one shape (every video, equal-size images) attend as one batch, mixed
|
| 261 |
+
# shapes frame by frame. `frame_shapes` gives the lengths without a device sync.
|
| 262 |
+
if frame_shapes is not None:
|
| 263 |
+
lengths = [height * width for height, width in frame_shapes]
|
| 264 |
+
else:
|
| 265 |
+
lengths = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
|
| 266 |
+
if len(set(lengths)) == 1:
|
| 267 |
+
query_states, key_states, value_states = (
|
| 268 |
+
tensor.reshape(-1, len(lengths), lengths[0], self.head_dim).transpose(0, 1)
|
| 269 |
+
for tensor in (query_states, key_states, value_states)
|
| 270 |
+
)
|
| 271 |
+
attn_output, attn_weights = attention_interface(
|
| 272 |
+
self,
|
| 273 |
+
query_states,
|
| 274 |
+
key_states,
|
| 275 |
+
value_states,
|
| 276 |
+
attention_mask=None,
|
| 277 |
+
scaling=self.scaling,
|
| 278 |
+
dropout=0.0,
|
| 279 |
+
is_causal=False,
|
| 280 |
+
**kwargs,
|
| 281 |
+
)
|
| 282 |
+
else:
|
| 283 |
+
splits = [torch.split(tensor, lengths, dim=2) for tensor in (query_states, key_states, value_states)]
|
| 284 |
+
attn_outputs, attn_weights = [], []
|
| 285 |
+
for q, k, v in zip(*splits):
|
| 286 |
+
output, weights = attention_interface(
|
| 287 |
+
self,
|
| 288 |
+
q,
|
| 289 |
+
k,
|
| 290 |
+
v,
|
| 291 |
+
attention_mask=None,
|
| 292 |
+
scaling=self.scaling,
|
| 293 |
+
dropout=0.0,
|
| 294 |
+
is_causal=False,
|
| 295 |
+
**kwargs,
|
| 296 |
+
)
|
| 297 |
+
attn_outputs.append(output)
|
| 298 |
+
attn_weights.append(weights)
|
| 299 |
+
attn_output = torch.cat(attn_outputs, dim=1)
|
| 300 |
+
# frames of different shapes have differently sized maps: one entry per frame, or None under sdpa
|
| 301 |
+
attn_weights = None if attn_weights[0] is None else tuple(attn_weights)
|
| 302 |
+
|
| 303 |
+
attn_output = attn_output.reshape(seq_length, -1).contiguous()
|
| 304 |
+
attn_output = self.out_proj(attn_output)
|
| 305 |
+
return attn_output, attn_weights
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class Lfm3VlVisionConv2d(nn.Module):
|
| 309 |
+
"""Gated depthwise 2D convolution over the patch grid of each frame."""
|
| 310 |
+
|
| 311 |
+
def __init__(self, config: Lfm3VlVisionConfig):
|
| 312 |
+
super().__init__()
|
| 313 |
+
self.spatial_merge_size = config.spatial_merge_size
|
| 314 |
+
self.in_proj = nn.Linear(config.hidden_size, 3 * config.hidden_size, bias=False)
|
| 315 |
+
self.spatial_conv = nn.Conv2d(
|
| 316 |
+
config.hidden_size,
|
| 317 |
+
config.hidden_size,
|
| 318 |
+
kernel_size=config.conv_kernel_size,
|
| 319 |
+
padding=config.conv_kernel_size // 2,
|
| 320 |
+
groups=config.hidden_size,
|
| 321 |
+
bias=False,
|
| 322 |
+
)
|
| 323 |
+
self.out_proj = nn.Linear(config.hidden_size, config.hidden_size, bias=False)
|
| 324 |
+
|
| 325 |
+
def forward(self, hidden_states: torch.Tensor, frame_shapes: list[list[int]]):
|
| 326 |
+
B, C, x = self.in_proj(hidden_states).chunk(3, dim=-1)
|
| 327 |
+
gated = B * x
|
| 328 |
+
merge = self.spatial_merge_size
|
| 329 |
+
num_frames = len(frame_shapes)
|
| 330 |
+
if len(set(map(tuple, frame_shapes))) == 1:
|
| 331 |
+
# frames of one shape (every video, equal-size images) convolve as one batch: merge-block order -> raster
|
| 332 |
+
# grid, convolve, back
|
| 333 |
+
height, width = frame_shapes[0]
|
| 334 |
+
frames = gated.view(num_frames, height // merge, width // merge, merge, merge, -1)
|
| 335 |
+
frames = frames.permute(0, 5, 1, 3, 2, 4).reshape(num_frames, -1, height, width)
|
| 336 |
+
frames = self.spatial_conv(frames)
|
| 337 |
+
frames = frames.view(num_frames, -1, height // merge, merge, width // merge, merge).permute(
|
| 338 |
+
0, 2, 4, 3, 5, 1
|
| 339 |
+
)
|
| 340 |
+
conv_output = frames.reshape(gated.shape[0], -1)
|
| 341 |
+
else:
|
| 342 |
+
conv_outputs = []
|
| 343 |
+
lengths = [height * width for height, width in frame_shapes]
|
| 344 |
+
for frame, (height, width) in zip(torch.split(gated, lengths, dim=0), frame_shapes):
|
| 345 |
+
frame = frame.view(height // merge, width // merge, merge, merge, -1)
|
| 346 |
+
frame = frame.permute(4, 0, 2, 1, 3).reshape(1, -1, height, width)
|
| 347 |
+
frame = self.spatial_conv(frame)
|
| 348 |
+
frame = frame.view(-1, height // merge, merge, width // merge, merge).permute(1, 3, 2, 4, 0)
|
| 349 |
+
conv_outputs.append(frame.reshape(height * width, -1))
|
| 350 |
+
conv_output = torch.cat(conv_outputs, dim=0)
|
| 351 |
+
return self.out_proj(C * conv_output)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class Lfm3VlVisionLayer(GradientCheckpointingLayer):
|
| 355 |
+
def __init__(self, config: Lfm3VlVisionConfig, layer_idx: int):
|
| 356 |
+
super().__init__()
|
| 357 |
+
self.is_attention_layer = config.layer_types[layer_idx] == "full_attention"
|
| 358 |
+
if self.is_attention_layer:
|
| 359 |
+
self.self_attn = Lfm3VlVisionAttention(config)
|
| 360 |
+
else:
|
| 361 |
+
self.conv = Lfm3VlVisionConv2d(config)
|
| 362 |
+
self.feed_forward = Lfm3VlMLP(config)
|
| 363 |
+
self.operator_norm = Lfm3VlRMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 364 |
+
self.ffn_norm = Lfm3VlRMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 365 |
+
|
| 366 |
+
def forward(
|
| 367 |
+
self,
|
| 368 |
+
hidden_states: torch.Tensor,
|
| 369 |
+
cu_seqlens: torch.Tensor,
|
| 370 |
+
frame_shapes: list[list[int]],
|
| 371 |
+
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
|
| 372 |
+
**kwargs,
|
| 373 |
+
) -> torch.Tensor:
|
| 374 |
+
if self.is_attention_layer:
|
| 375 |
+
operator_output, _ = self.self_attn(
|
| 376 |
+
self.operator_norm(hidden_states),
|
| 377 |
+
cu_seqlens=cu_seqlens,
|
| 378 |
+
position_embeddings=position_embeddings,
|
| 379 |
+
frame_shapes=frame_shapes,
|
| 380 |
+
**kwargs,
|
| 381 |
+
)
|
| 382 |
+
else:
|
| 383 |
+
operator_output = self.conv(self.operator_norm(hidden_states), frame_shapes)
|
| 384 |
+
hidden_states = hidden_states + operator_output
|
| 385 |
+
hidden_states = hidden_states + self.feed_forward(self.ffn_norm(hidden_states))
|
| 386 |
+
return hidden_states
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def create_masks_by_layer_type(
|
| 390 |
+
config: PreTrainedConfig,
|
| 391 |
+
inputs_embeds: torch.Tensor,
|
| 392 |
+
attention_mask: torch.Tensor | None,
|
| 393 |
+
past_key_values: Cache | None,
|
| 394 |
+
position_ids: torch.Tensor | None = None,
|
| 395 |
+
) -> dict:
|
| 396 |
+
"""The cache sizes each mask from the layer it is asked about, and the generic mask utilities ask about the first
|
| 397 |
+
non-sliding layer, which for a leading `conv` layer redirects to the first attention layer of any type: name the
|
| 398 |
+
first layer of each mask's own type instead."""
|
| 399 |
+
config = config.get_text_config()
|
| 400 |
+
mask_kwargs = {
|
| 401 |
+
"config": config,
|
| 402 |
+
"inputs_embeds": inputs_embeds,
|
| 403 |
+
"attention_mask": attention_mask,
|
| 404 |
+
"past_key_values": past_key_values,
|
| 405 |
+
"position_ids": position_ids,
|
| 406 |
+
}
|
| 407 |
+
layer_types, causal_mask_mapping = config.layer_types, {}
|
| 408 |
+
if "full_attention" in layer_types:
|
| 409 |
+
layer_idx = layer_types.index("full_attention")
|
| 410 |
+
causal_mask_mapping["full_attention"] = create_causal_mask(**mask_kwargs, layer_idx=layer_idx)
|
| 411 |
+
if "sliding_attention" in layer_types:
|
| 412 |
+
layer_idx = layer_types.index("sliding_attention")
|
| 413 |
+
causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(
|
| 414 |
+
**mask_kwargs, layer_idx=layer_idx
|
| 415 |
+
)
|
| 416 |
+
if "conv" in layer_types:
|
| 417 |
+
causal_mask_mapping["conv"] = create_recurrent_attention_mask(**mask_kwargs)
|
| 418 |
+
return causal_mask_mapping
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
@auto_docstring
|
| 422 |
+
class Lfm3VlPreTrainedModel(PreTrainedModel):
|
| 423 |
+
config: Lfm3VlConfig
|
| 424 |
+
base_model_prefix = "model"
|
| 425 |
+
input_modalities = ("image", "video", "text")
|
| 426 |
+
supports_gradient_checkpointing = True
|
| 427 |
+
_skip_keys_device_placement = ["past_key_values"]
|
| 428 |
+
|
| 429 |
+
_supports_flash_attn = True
|
| 430 |
+
_supports_sdpa = True
|
| 431 |
+
# the cached decode step traces without graph breaks, like the text model
|
| 432 |
+
_can_compile_fullgraph = True
|
| 433 |
+
_supports_flex_attn = True
|
| 434 |
+
_supports_attention_backend = True
|
| 435 |
+
_no_split_modules = ["Lfm3VlVisionLayer"]
|
| 436 |
+
|
| 437 |
+
@staticmethod
|
| 438 |
+
def create_masks_for_generate(
|
| 439 |
+
config: PreTrainedConfig,
|
| 440 |
+
inputs_embeds: torch.Tensor,
|
| 441 |
+
attention_mask: torch.Tensor | None,
|
| 442 |
+
past_key_values: Cache | None,
|
| 443 |
+
position_ids: torch.Tensor | None = None,
|
| 444 |
+
**kwargs,
|
| 445 |
+
) -> dict:
|
| 446 |
+
# the static-cache masks of the text model are sized by layer type, not by the generic first attention layer
|
| 447 |
+
return create_masks_by_layer_type(config, inputs_embeds, attention_mask, past_key_values, position_ids)
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def get_vision_frame_shapes(grid_thw: torch.Tensor) -> list[list[int]]:
|
| 451 |
+
return grid_thw[:, 1:].repeat_interleave(grid_thw[:, 0], dim=0).tolist()
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
@auto_docstring(
|
| 455 |
+
custom_intro="""
|
| 456 |
+
The LFM3-VL vision encoder: a stack of gated convolution and bidirectional attention layers over the patches of
|
| 457 |
+
each image or video frame, embedded by a linear patchifier.
|
| 458 |
+
"""
|
| 459 |
+
)
|
| 460 |
+
class Lfm3VlVisionModel(Lfm3VlPreTrainedModel):
|
| 461 |
+
config: Lfm3VlVisionConfig
|
| 462 |
+
input_modalities = ("image", "video")
|
| 463 |
+
_input_embed_layer = "patchifier"
|
| 464 |
+
_can_record_outputs = {
|
| 465 |
+
"hidden_states": Lfm3VlVisionLayer,
|
| 466 |
+
"attentions": OutputRecorder(Lfm3VlVisionAttention, index=1),
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
def __init__(self, config: Lfm3VlVisionConfig):
|
| 470 |
+
super().__init__(config)
|
| 471 |
+
self.spatial_merge_size = config.spatial_merge_size
|
| 472 |
+
patch_dim = config.in_channels * config.temporal_patch_size * config.patch_size**2
|
| 473 |
+
self.patchifier = nn.Linear(patch_dim, config.hidden_size, bias=False)
|
| 474 |
+
self.embedding_norm = Lfm3VlRMSNorm(config.hidden_size, eps=config.norm_eps)
|
| 475 |
+
self.rotary_pos_emb = Lfm3VlVisionRotaryEmbedding(config)
|
| 476 |
+
self.layers = nn.ModuleList(
|
| 477 |
+
[Lfm3VlVisionLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
|
| 478 |
+
)
|
| 479 |
+
self.gradient_checkpointing = False
|
| 480 |
+
self.post_init()
|
| 481 |
+
|
| 482 |
+
@merge_with_config_defaults
|
| 483 |
+
@capture_outputs
|
| 484 |
+
@auto_docstring
|
| 485 |
+
def forward(
|
| 486 |
+
self, pixel_values: torch.Tensor, grid_thw: torch.Tensor, **kwargs: Unpack[TransformersKwargs]
|
| 487 |
+
) -> BaseModelOutputWithPooling:
|
| 488 |
+
r"""
|
| 489 |
+
pixel_values (`torch.Tensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`):
|
| 490 |
+
The flattened patches of every image or video, grouped by merge block.
|
| 491 |
+
grid_thw (`torch.LongTensor` of shape `(num_images_or_videos, 3)`):
|
| 492 |
+
The temporal, height and width dimensions of the patch grid of each image or video.
|
| 493 |
+
"""
|
| 494 |
+
position_ids = get_vision_position_ids(grid_thw, self.spatial_merge_size, kwargs=kwargs)
|
| 495 |
+
cu_seqlens, max_seqlen = get_vision_attention_seqlens(grid_thw, self.config, kwargs=kwargs)
|
| 496 |
+
frame_shapes = get_vision_frame_shapes(grid_thw)
|
| 497 |
+
|
| 498 |
+
hidden_states = self.patchifier(pixel_values.to(self.patchifier.weight.dtype))
|
| 499 |
+
hidden_states = self.embedding_norm(hidden_states)
|
| 500 |
+
position_embeddings = self.rotary_pos_emb(hidden_states, position_ids)
|
| 501 |
+
|
| 502 |
+
for layer in self.layers:
|
| 503 |
+
hidden_states = layer(
|
| 504 |
+
hidden_states,
|
| 505 |
+
cu_seqlens=cu_seqlens,
|
| 506 |
+
frame_shapes=frame_shapes,
|
| 507 |
+
max_seqlen=max_seqlen,
|
| 508 |
+
position_embeddings=position_embeddings,
|
| 509 |
+
**kwargs,
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
return BaseModelOutputWithPooling(last_hidden_state=hidden_states)
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
class Lfm3VlMultiModalProjector(nn.Module):
|
| 516 |
+
"""Merges each block of `downsample_factor**2` vision tokens into one language token."""
|
| 517 |
+
|
| 518 |
+
def __init__(self, config: Lfm3VlConfig):
|
| 519 |
+
super().__init__()
|
| 520 |
+
self.downsample_factor = config.downsample_factor
|
| 521 |
+
self.layer_norm = nn.LayerNorm(config.vision_config.hidden_size, eps=config.projector_layer_norm_eps)
|
| 522 |
+
self.linear_1 = nn.Linear(
|
| 523 |
+
config.vision_config.hidden_size * config.downsample_factor**2,
|
| 524 |
+
config.projector_hidden_size,
|
| 525 |
+
bias=config.projector_bias,
|
| 526 |
+
)
|
| 527 |
+
self.act = ACT2FN[config.projector_hidden_act]
|
| 528 |
+
self.linear_2 = nn.Linear(
|
| 529 |
+
config.projector_hidden_size, config.text_config.hidden_size, bias=config.projector_bias
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
def forward(self, image_features: torch.Tensor):
|
| 533 |
+
hidden_states = self.layer_norm(image_features).reshape(-1, self.linear_1.in_features)
|
| 534 |
+
hidden_states = self.linear_1(hidden_states)
|
| 535 |
+
hidden_states = self.act(hidden_states)
|
| 536 |
+
hidden_states = self.linear_2(hidden_states)
|
| 537 |
+
return hidden_states
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
@auto_docstring(
|
| 541 |
+
custom_intro="""
|
| 542 |
+
Base class for Lfm3Vl outputs, with hidden states and attentions.
|
| 543 |
+
"""
|
| 544 |
+
)
|
| 545 |
+
@dataclass
|
| 546 |
+
class Lfm3VlModelOutputWithPast(BaseModelOutputWithPast):
|
| 547 |
+
r"""
|
| 548 |
+
past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`):
|
| 549 |
+
It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache).
|
| 550 |
+
|
| 551 |
+
Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see
|
| 552 |
+
`past_key_values` input) to speed up sequential decoding.
|
| 553 |
+
image_hidden_states (`torch.FloatTensor`, *optional*):
|
| 554 |
+
A `torch.FloatTensor` of size `(num_image_tokens, hidden_size)`.
|
| 555 |
+
image_hidden_states of the model produced by the vision encoder and after projecting the last hidden state.
|
| 556 |
+
video_hidden_states (`torch.FloatTensor`, *optional*):
|
| 557 |
+
A `torch.FloatTensor` of size `(num_video_tokens, hidden_size)`.
|
| 558 |
+
video_hidden_states of the model produced by the vision encoder and after projecting the last hidden state.
|
| 559 |
+
"""
|
| 560 |
+
|
| 561 |
+
image_hidden_states: torch.FloatTensor | None = None
|
| 562 |
+
video_hidden_states: torch.FloatTensor | None = None
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
@auto_docstring(
|
| 566 |
+
custom_intro="""
|
| 567 |
+
Base class for Lfm3Vl causal language model (or autoregressive) outputs.
|
| 568 |
+
"""
|
| 569 |
+
)
|
| 570 |
+
@dataclass
|
| 571 |
+
class Lfm3VlCausalLMOutputWithPast(CausalLMOutputWithPast):
|
| 572 |
+
r"""
|
| 573 |
+
image_hidden_states (`torch.FloatTensor`, *optional*):
|
| 574 |
+
A `torch.FloatTensor` of size `(num_image_tokens, hidden_size)`.
|
| 575 |
+
image_hidden_states of the model produced by the vision encoder and after projecting the last hidden state.
|
| 576 |
+
video_hidden_states (`torch.FloatTensor`, *optional*):
|
| 577 |
+
A `torch.FloatTensor` of size `(num_video_tokens, hidden_size)`.
|
| 578 |
+
video_hidden_states of the model produced by the vision encoder and after projecting the last hidden state.
|
| 579 |
+
"""
|
| 580 |
+
|
| 581 |
+
image_hidden_states: torch.FloatTensor | None = None
|
| 582 |
+
video_hidden_states: torch.FloatTensor | None = None
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
@auto_docstring(
|
| 586 |
+
custom_intro="""
|
| 587 |
+
The Lfm3Vl model which consists of a vision backbone and a language model, without a language modeling head.
|
| 588 |
+
"""
|
| 589 |
+
)
|
| 590 |
+
class Lfm3VlModel(Lfm3VlPreTrainedModel):
|
| 591 |
+
def __init__(self, config: Lfm3VlConfig):
|
| 592 |
+
super().__init__(config)
|
| 593 |
+
self.vision_tower = Lfm3VlVisionModel(config.vision_config)
|
| 594 |
+
self.multi_modal_projector = Lfm3VlMultiModalProjector(config)
|
| 595 |
+
self.language_model = Lfm3Model(config.text_config)
|
| 596 |
+
self.post_init()
|
| 597 |
+
|
| 598 |
+
@accepts_precomputed_kwargs(modality="image")
|
| 599 |
+
@can_return_tuple
|
| 600 |
+
@auto_docstring(
|
| 601 |
+
custom_intro="Obtains image last hidden states from the vision tower and apply multimodal projection."
|
| 602 |
+
)
|
| 603 |
+
def get_image_features(
|
| 604 |
+
self,
|
| 605 |
+
pixel_values: torch.FloatTensor,
|
| 606 |
+
image_grid_thw: torch.LongTensor,
|
| 607 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 608 |
+
) -> tuple | BaseModelOutputWithPooling:
|
| 609 |
+
r"""
|
| 610 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`):
|
| 611 |
+
The flattened patches of every image, grouped by merge block.
|
| 612 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`):
|
| 613 |
+
The temporal, height and width dimensions of the patch grid of each image.
|
| 614 |
+
"""
|
| 615 |
+
image_outputs = self.vision_tower(pixel_values, grid_thw=image_grid_thw, return_dict=True, **kwargs)
|
| 616 |
+
image_features = self.multi_modal_projector(image_outputs.last_hidden_state)
|
| 617 |
+
split_sizes = (image_grid_thw.prod(-1) // self.vision_tower.spatial_merge_size**2).tolist()
|
| 618 |
+
image_outputs.pooler_output = torch.split(image_features, split_sizes)
|
| 619 |
+
return image_outputs
|
| 620 |
+
|
| 621 |
+
def get_placeholder_mask(
|
| 622 |
+
self,
|
| 623 |
+
input_ids: torch.LongTensor,
|
| 624 |
+
inputs_embeds: torch.FloatTensor,
|
| 625 |
+
image_features: torch.FloatTensor | None = None,
|
| 626 |
+
video_features: torch.FloatTensor | None = None,
|
| 627 |
+
):
|
| 628 |
+
"""Placeholder masks from `input_ids` or `inputs_embeds`, checked against the feature counts."""
|
| 629 |
+
if input_ids is None:
|
| 630 |
+
special_image_mask = inputs_embeds == self.get_input_embeddings()(
|
| 631 |
+
torch.full((), self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device)
|
| 632 |
+
)
|
| 633 |
+
special_image_mask = special_image_mask.all(-1)
|
| 634 |
+
special_video_mask = inputs_embeds == self.get_input_embeddings()(
|
| 635 |
+
torch.full((), self.config.video_token_id, dtype=torch.long, device=inputs_embeds.device)
|
| 636 |
+
)
|
| 637 |
+
special_video_mask = special_video_mask.all(-1)
|
| 638 |
+
else:
|
| 639 |
+
special_image_mask = input_ids == self.config.image_token_id
|
| 640 |
+
special_video_mask = input_ids == self.config.video_token_id
|
| 641 |
+
|
| 642 |
+
n_image_tokens = special_image_mask.sum()
|
| 643 |
+
special_image_mask = special_image_mask.unsqueeze(-1).to(inputs_embeds.device)
|
| 644 |
+
if image_features is not None:
|
| 645 |
+
torch_compilable_check(
|
| 646 |
+
n_image_tokens * inputs_embeds.shape[-1] == image_features.numel(),
|
| 647 |
+
f"Image features and image tokens do not match, tokens: {n_image_tokens}, features: {image_features.shape[0]}",
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
n_video_tokens = special_video_mask.sum()
|
| 651 |
+
special_video_mask = special_video_mask.unsqueeze(-1).to(inputs_embeds.device)
|
| 652 |
+
if video_features is not None:
|
| 653 |
+
torch_compilable_check(
|
| 654 |
+
n_video_tokens * inputs_embeds.shape[-1] == video_features.numel(),
|
| 655 |
+
f"Video features and video tokens do not match, tokens: {n_video_tokens}, features: {video_features.shape[0]}",
|
| 656 |
+
)
|
| 657 |
+
return special_image_mask, special_video_mask
|
| 658 |
+
|
| 659 |
+
@can_return_tuple
|
| 660 |
+
@auto_docstring
|
| 661 |
+
def forward(
|
| 662 |
+
self,
|
| 663 |
+
input_ids: torch.LongTensor | None = None,
|
| 664 |
+
attention_mask: torch.Tensor | None = None,
|
| 665 |
+
position_ids: torch.LongTensor | None = None,
|
| 666 |
+
past_key_values: Cache | None = None,
|
| 667 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 668 |
+
pixel_values: torch.FloatTensor | None = None,
|
| 669 |
+
image_grid_thw: torch.LongTensor | None = None,
|
| 670 |
+
pixel_values_videos: torch.FloatTensor | None = None,
|
| 671 |
+
video_grid_thw: torch.LongTensor | None = None,
|
| 672 |
+
use_cache: bool | None = None,
|
| 673 |
+
mm_encoder_outputs: dict[str, BaseModelOutputWithPooling] | None = None,
|
| 674 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 675 |
+
) -> tuple | Lfm3VlModelOutputWithPast:
|
| 676 |
+
r"""
|
| 677 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`, *optional*):
|
| 678 |
+
The flattened patches of every image, grouped by merge block.
|
| 679 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 680 |
+
The temporal, height and width dimensions of the patch grid of each image.
|
| 681 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`, *optional*):
|
| 682 |
+
The flattened patches of every video, grouped by merge block.
|
| 683 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
|
| 684 |
+
The temporal, height and width dimensions of the patch grid of each video.
|
| 685 |
+
"""
|
| 686 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 687 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 688 |
+
|
| 689 |
+
if (pixel_values is not None or pixel_values_videos is not None) and mm_encoder_outputs is not None:
|
| 690 |
+
raise ValueError(
|
| 691 |
+
"You cannot specify both pixel_values/pixel_values_videos and mm_encoder_outputs at the same time"
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
if inputs_embeds is None:
|
| 695 |
+
inputs_embeds = self.get_input_embeddings()(input_ids)
|
| 696 |
+
|
| 697 |
+
mm_encoder_outputs = mm_encoder_outputs if mm_encoder_outputs is not None else {}
|
| 698 |
+
if (pixel_values is None) != (image_grid_thw is None):
|
| 699 |
+
raise ValueError("pixel_values and image_grid_thw must be supplied together")
|
| 700 |
+
if (pixel_values_videos is None) != (video_grid_thw is None):
|
| 701 |
+
raise ValueError("pixel_values_videos and video_grid_thw must be supplied together")
|
| 702 |
+
if mm_encoder_outputs.get("image") is None and pixel_values is not None:
|
| 703 |
+
mm_encoder_outputs["image"] = self.get_image_features(
|
| 704 |
+
pixel_values, image_grid_thw, return_dict=True, **kwargs
|
| 705 |
+
)
|
| 706 |
+
if mm_encoder_outputs.get("video") is None and pixel_values_videos is not None:
|
| 707 |
+
mm_encoder_outputs["video"] = self.get_video_features(
|
| 708 |
+
pixel_values_videos, video_grid_thw, return_dict=True, **kwargs
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
image_features = None
|
| 712 |
+
if mm_encoder_outputs.get("image") is not None:
|
| 713 |
+
image_features = torch.cat(mm_encoder_outputs["image"].pooler_output, dim=0).to(
|
| 714 |
+
inputs_embeds.device, inputs_embeds.dtype
|
| 715 |
+
)
|
| 716 |
+
image_mask, _ = self.get_placeholder_mask(
|
| 717 |
+
input_ids, inputs_embeds=inputs_embeds, image_features=image_features
|
| 718 |
+
)
|
| 719 |
+
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_features)
|
| 720 |
+
|
| 721 |
+
video_features = None
|
| 722 |
+
if mm_encoder_outputs.get("video") is not None:
|
| 723 |
+
video_features = torch.cat(mm_encoder_outputs["video"].pooler_output, dim=0).to(
|
| 724 |
+
inputs_embeds.device, inputs_embeds.dtype
|
| 725 |
+
)
|
| 726 |
+
_, video_mask = self.get_placeholder_mask(
|
| 727 |
+
input_ids, inputs_embeds=inputs_embeds, video_features=video_features
|
| 728 |
+
)
|
| 729 |
+
inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_features)
|
| 730 |
+
|
| 731 |
+
outputs = self.language_model(
|
| 732 |
+
attention_mask=attention_mask,
|
| 733 |
+
position_ids=position_ids,
|
| 734 |
+
past_key_values=past_key_values,
|
| 735 |
+
inputs_embeds=inputs_embeds,
|
| 736 |
+
use_cache=use_cache,
|
| 737 |
+
**kwargs,
|
| 738 |
+
)
|
| 739 |
+
|
| 740 |
+
return Lfm3VlModelOutputWithPast(
|
| 741 |
+
last_hidden_state=outputs.last_hidden_state,
|
| 742 |
+
past_key_values=outputs.past_key_values,
|
| 743 |
+
hidden_states=outputs.hidden_states,
|
| 744 |
+
attentions=outputs.attentions,
|
| 745 |
+
image_hidden_states=image_features,
|
| 746 |
+
video_hidden_states=video_features,
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
@accepts_precomputed_kwargs(modality="video")
|
| 750 |
+
@can_return_tuple
|
| 751 |
+
@auto_docstring(
|
| 752 |
+
custom_intro="Obtains video last hidden states from the vision tower and apply multimodal projection."
|
| 753 |
+
)
|
| 754 |
+
def get_video_features(
|
| 755 |
+
self,
|
| 756 |
+
pixel_values_videos: torch.FloatTensor,
|
| 757 |
+
video_grid_thw: torch.LongTensor,
|
| 758 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 759 |
+
) -> tuple | BaseModelOutputWithPooling:
|
| 760 |
+
r"""
|
| 761 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`):
|
| 762 |
+
The flattened patches of every video, grouped by merge block.
|
| 763 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`):
|
| 764 |
+
The temporal, height and width dimensions of the patch grid of each video.
|
| 765 |
+
"""
|
| 766 |
+
return self.get_image_features(pixel_values_videos, video_grid_thw, **kwargs)
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
def reject_packed_generation(config: PreTrainedConfig, input_ids, generation_config, model_kwargs, is_first_iteration):
|
| 770 |
+
"""`generate` decodes from the cache its prefill fills, and no mask it builds isolates packed documents
|
| 771 |
+
(`position_ids` restarting): the prompt is refused before the prefill, one forward or every chunk, writes to it."""
|
| 772 |
+
position_ids, attention_mask = model_kwargs.get("position_ids"), model_kwargs.get("attention_mask")
|
| 773 |
+
cache = model_kwargs.get("past_key_values")
|
| 774 |
+
if cache is None or position_ids is None or position_ids.ndim != 2:
|
| 775 |
+
return
|
| 776 |
+
if attention_mask is not None and attention_mask.ndim != 2:
|
| 777 |
+
return
|
| 778 |
+
has_attention = any(t != "conv" for t in config.get_text_config().layer_types)
|
| 779 |
+
filled = bool(cache.get_seq_length() > 0 if has_attention else cache.has_previous_state())
|
| 780 |
+
inputs_embeds = model_kwargs.get("inputs_embeds") if is_first_iteration else None
|
| 781 |
+
length = input_ids.shape[1] if inputs_embeds is None else inputs_embeds.shape[1]
|
| 782 |
+
if generation_config.prefill_chunk_size is not None:
|
| 783 |
+
# the chunks run from the start of the prompt
|
| 784 |
+
position_ids = position_ids[:, :length]
|
| 785 |
+
else:
|
| 786 |
+
if filled and has_attention:
|
| 787 |
+
# the slice `_prefill` feeds: a prompt that spans the cache is cut to its uncached part
|
| 788 |
+
mask_length = getattr(generation_config, "_mask_length", -1)
|
| 789 |
+
if attention_mask is not None:
|
| 790 |
+
mask_length = attention_mask.shape[1]
|
| 791 |
+
if inputs_embeds is not None or mask_length == length:
|
| 792 |
+
length -= int(cache.get_seq_length())
|
| 793 |
+
position_ids = position_ids[:, position_ids.shape[1] - length :]
|
| 794 |
+
if attention_mask is None:
|
| 795 |
+
attention_mask = torch.ones_like(position_ids)
|
| 796 |
+
if bool(packed_under_padding(position_ids, attention_mask, filled).any()):
|
| 797 |
+
raise ValueError(
|
| 798 |
+
"`generate` cannot decode packed documents (`position_ids` restarting, or starting at 0 on a cache that "
|
| 799 |
+
"holds tokens): attention would span them. Generate each document in its own row of a left-padded batch."
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
def packed_under_padding(position_ids: torch.Tensor, padding_mask: torch.Tensor, filled) -> torch.Tensor:
|
| 804 |
+
"""Padding repeats position 0, so packing is a restart between two kept tokens, or a kept position 0 on a filled
|
| 805 |
+
cache whose row holds a kept token; attention under a padding mask cannot isolate it."""
|
| 806 |
+
restarts = torch.diff(position_ids, prepend=position_ids[:, :1] - 1, dim=-1) != 1
|
| 807 |
+
valid = padding_mask[:, -restarts.shape[-1] :].bool()
|
| 808 |
+
packed = restarts[:, 1:] & valid[:, 1:] & valid[:, :-1]
|
| 809 |
+
if filled:
|
| 810 |
+
# a row whose cached tokens are all padding (chunked prefill of a left-padded batch) has no document to pack
|
| 811 |
+
# against; a mask covering only the chunk keeps the strict rule
|
| 812 |
+
cached = padding_mask[:, : padding_mask.shape[-1] - restarts.shape[-1]]
|
| 813 |
+
cached_valid = cached.bool().any(-1, keepdim=True) if cached.shape[-1] else torch.ones_like(valid[:, :1])
|
| 814 |
+
packed = packed | (valid[:, :1] & (position_ids[:, :1] == 0) & cached_valid)
|
| 815 |
+
return packed
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
@auto_docstring(
|
| 819 |
+
custom_intro="""
|
| 820 |
+
The LFM3_VL model which consists of a vision backbone and a language model.
|
| 821 |
+
"""
|
| 822 |
+
)
|
| 823 |
+
class Lfm3VlForConditionalGeneration(Lfm3VlPreTrainedModel, GenerationMixin):
|
| 824 |
+
_tied_weights_keys = {"lm_head.weight": "model.language_model.embed_tokens.weight"}
|
| 825 |
+
|
| 826 |
+
def __init__(self, config: Lfm3VlConfig):
|
| 827 |
+
super().__init__(config)
|
| 828 |
+
self.model = Lfm3VlModel(config)
|
| 829 |
+
self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False)
|
| 830 |
+
self.post_init()
|
| 831 |
+
|
| 832 |
+
def get_output_embeddings(self) -> nn.Module:
|
| 833 |
+
return self.lm_head
|
| 834 |
+
|
| 835 |
+
@auto_docstring
|
| 836 |
+
def get_image_features(
|
| 837 |
+
self,
|
| 838 |
+
pixel_values: torch.FloatTensor,
|
| 839 |
+
image_grid_thw: torch.LongTensor,
|
| 840 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 841 |
+
) -> tuple | BaseModelOutputWithPooling:
|
| 842 |
+
r"""
|
| 843 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`):
|
| 844 |
+
The flattened patches of every image, grouped by merge block.
|
| 845 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`):
|
| 846 |
+
The temporal, height and width dimensions of the patch grid of each image.
|
| 847 |
+
"""
|
| 848 |
+
return self.model.get_image_features(pixel_values, image_grid_thw, **kwargs)
|
| 849 |
+
|
| 850 |
+
@can_return_tuple
|
| 851 |
+
@auto_docstring
|
| 852 |
+
def forward(
|
| 853 |
+
self,
|
| 854 |
+
input_ids: torch.LongTensor | None = None,
|
| 855 |
+
attention_mask: torch.Tensor | None = None,
|
| 856 |
+
position_ids: torch.LongTensor | None = None,
|
| 857 |
+
past_key_values: Cache | None = None,
|
| 858 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 859 |
+
pixel_values: torch.FloatTensor | None = None,
|
| 860 |
+
image_grid_thw: torch.LongTensor | None = None,
|
| 861 |
+
pixel_values_videos: torch.FloatTensor | None = None,
|
| 862 |
+
video_grid_thw: torch.LongTensor | None = None,
|
| 863 |
+
labels: torch.LongTensor | None = None,
|
| 864 |
+
use_cache: bool | None = None,
|
| 865 |
+
logits_to_keep: int | torch.Tensor = 0,
|
| 866 |
+
mm_encoder_outputs: dict[str, BaseModelOutputWithPooling] | None = None,
|
| 867 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 868 |
+
) -> tuple | Lfm3VlCausalLMOutputWithPast:
|
| 869 |
+
r"""
|
| 870 |
+
pixel_values (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`, *optional*):
|
| 871 |
+
The flattened patches of every image, grouped by merge block.
|
| 872 |
+
image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
|
| 873 |
+
The temporal, height and width dimensions of the patch grid of each image.
|
| 874 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`, *optional*):
|
| 875 |
+
The flattened patches of every video, grouped by merge block.
|
| 876 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
|
| 877 |
+
The temporal, height and width dimensions of the patch grid of each video.
|
| 878 |
+
labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
|
| 879 |
+
Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
|
| 880 |
+
config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
|
| 881 |
+
(masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
|
| 882 |
+
|
| 883 |
+
Example:
|
| 884 |
+
|
| 885 |
+
```python
|
| 886 |
+
>>> from transformers import AutoProcessor, AutoModelForImageTextToText
|
| 887 |
+
|
| 888 |
+
>>> model = AutoModelForImageTextToText.from_pretrained("LiquidAI/LFM3-VL-1.9B")
|
| 889 |
+
>>> processor = AutoProcessor.from_pretrained("LiquidAI/LFM3-VL-1.9B")
|
| 890 |
+
|
| 891 |
+
>>> conversation = [
|
| 892 |
+
... {
|
| 893 |
+
... "role": "user",
|
| 894 |
+
... "content": [
|
| 895 |
+
... {"type": "image", "url": "https://www.ilankelman.org/stopsigns/australia.jpg"},
|
| 896 |
+
... {"type": "text", "text": "What is in this image?"},
|
| 897 |
+
... ],
|
| 898 |
+
... },
|
| 899 |
+
... ]
|
| 900 |
+
|
| 901 |
+
>>> inputs = processor.apply_chat_template(
|
| 902 |
+
... conversation, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt"
|
| 903 |
+
... )
|
| 904 |
+
|
| 905 |
+
>>> outputs = model.generate(**inputs, max_new_tokens=45)
|
| 906 |
+
>>> processor.batch_decode(outputs, skip_special_tokens=True)[0]
|
| 907 |
+
```"""
|
| 908 |
+
outputs = self.model(
|
| 909 |
+
input_ids=input_ids,
|
| 910 |
+
attention_mask=attention_mask,
|
| 911 |
+
position_ids=position_ids,
|
| 912 |
+
past_key_values=past_key_values,
|
| 913 |
+
inputs_embeds=inputs_embeds,
|
| 914 |
+
pixel_values=pixel_values,
|
| 915 |
+
image_grid_thw=image_grid_thw,
|
| 916 |
+
pixel_values_videos=pixel_values_videos,
|
| 917 |
+
video_grid_thw=video_grid_thw,
|
| 918 |
+
use_cache=use_cache,
|
| 919 |
+
mm_encoder_outputs=mm_encoder_outputs,
|
| 920 |
+
**kwargs,
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
hidden_states = outputs[0]
|
| 924 |
+
# Only compute necessary logits, and do not upcast them to float if we are not computing the loss
|
| 925 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 926 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 927 |
+
|
| 928 |
+
loss = None
|
| 929 |
+
if labels is not None:
|
| 930 |
+
loss = self.loss_function(
|
| 931 |
+
logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size, **kwargs
|
| 932 |
+
)
|
| 933 |
+
|
| 934 |
+
return Lfm3VlCausalLMOutputWithPast(
|
| 935 |
+
loss=loss,
|
| 936 |
+
logits=logits,
|
| 937 |
+
past_key_values=outputs.past_key_values,
|
| 938 |
+
hidden_states=outputs.hidden_states,
|
| 939 |
+
attentions=outputs.attentions,
|
| 940 |
+
image_hidden_states=outputs.image_hidden_states,
|
| 941 |
+
video_hidden_states=outputs.video_hidden_states,
|
| 942 |
+
)
|
| 943 |
+
|
| 944 |
+
def _prefill(self, input_ids, generation_config, model_kwargs, is_first_iteration=True):
|
| 945 |
+
# the text model's refusal, before the prefill writes to the cache
|
| 946 |
+
reject_packed_generation(self.config, input_ids, generation_config, model_kwargs, is_first_iteration)
|
| 947 |
+
return super()._prefill(input_ids, generation_config, model_kwargs, is_first_iteration=is_first_iteration)
|
| 948 |
+
|
| 949 |
+
@auto_docstring
|
| 950 |
+
def get_video_features(
|
| 951 |
+
self,
|
| 952 |
+
pixel_values_videos: torch.FloatTensor,
|
| 953 |
+
video_grid_thw: torch.LongTensor,
|
| 954 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 955 |
+
) -> tuple | BaseModelOutputWithPooling:
|
| 956 |
+
r"""
|
| 957 |
+
pixel_values_videos (`torch.FloatTensor` of shape `(num_patches, in_channels * temporal_patch_size * patch_size**2)`):
|
| 958 |
+
The flattened patches of every video, grouped by merge block.
|
| 959 |
+
video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`):
|
| 960 |
+
The temporal, height and width dimensions of the patch grid of each video.
|
| 961 |
+
"""
|
| 962 |
+
return self.model.get_video_features(pixel_values_videos, video_grid_thw, **kwargs)
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
__all__ = ["Lfm3VlForConditionalGeneration", "Lfm3VlModel", "Lfm3VlPreTrainedModel", "Lfm3VlVisionModel"]
|
modeling_liquid_codec.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from torch import nn
|
| 21 |
+
|
| 22 |
+
from transformers import initialization as init
|
| 23 |
+
from transformers.cache_utils import DynamicCache
|
| 24 |
+
from transformers.conversion_mapping import register_checkpoint_conversion_mapping
|
| 25 |
+
from transformers.core_model_loading import WeightRenaming
|
| 26 |
+
from transformers.modeling_utils import PreTrainedAudioTokenizerBase
|
| 27 |
+
from transformers.utils import ModelOutput, auto_docstring, can_return_tuple, torch_compilable_check
|
| 28 |
+
from transformers.utils.import_utils import is_tracing
|
| 29 |
+
|
| 30 |
+
from .configuration_liquid_codec import LiquidCodecConfig
|
| 31 |
+
from .modeling_lfm3 import Lfm3Model
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@auto_docstring
|
| 35 |
+
@dataclass
|
| 36 |
+
class LiquidCodecEncoderOutput(ModelOutput):
|
| 37 |
+
r"""
|
| 38 |
+
audio_codes (`torch.LongTensor` of shape `(batch_size, num_codebooks, num_frames)`):
|
| 39 |
+
The code of every codebook of every frame.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
audio_codes: torch.LongTensor = None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@auto_docstring
|
| 46 |
+
@dataclass
|
| 47 |
+
class LiquidCodecDecoderOutput(ModelOutput):
|
| 48 |
+
r"""
|
| 49 |
+
audio_values (`torch.FloatTensor` of shape `(batch_size, num_samples)`):
|
| 50 |
+
The decoded 24 kHz audio in float32.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
audio_values: torch.FloatTensor = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@auto_docstring
|
| 57 |
+
@dataclass
|
| 58 |
+
class LiquidCodecOutput(ModelOutput):
|
| 59 |
+
r"""
|
| 60 |
+
audio_values (`torch.FloatTensor` of shape `(batch_size, num_samples)`):
|
| 61 |
+
The reconstructed audio, cut to the input length.
|
| 62 |
+
audio_codes (`torch.LongTensor` of shape `(batch_size, num_codebooks, num_frames)`):
|
| 63 |
+
The code of every codebook of every frame.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
audio_values: torch.FloatTensor = None
|
| 67 |
+
audio_codes: torch.LongTensor = None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class LiquidCodecStreamingCache:
|
| 71 |
+
"""Carries the decoder state between chunks: the LFM3 cache and the unfinished overlap-add tail."""
|
| 72 |
+
|
| 73 |
+
def __init__(self, config: LiquidCodecConfig):
|
| 74 |
+
self.past_key_values = DynamicCache(config=config.lfm_config(config.decoder_window))
|
| 75 |
+
self.audio: torch.Tensor | None = None
|
| 76 |
+
self.envelope: torch.Tensor | None = None
|
| 77 |
+
self.started = False
|
| 78 |
+
self.finished = False
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class LiquidCodecSTFT(nn.Module):
|
| 82 |
+
def __init__(self, config: LiquidCodecConfig):
|
| 83 |
+
super().__init__()
|
| 84 |
+
self.n_fft = config.stft_n_fft
|
| 85 |
+
self.hop_length = config.stft_hop_length
|
| 86 |
+
self.win_length = config.stft_win_length
|
| 87 |
+
self.window = nn.Buffer(torch.hann_window(self.win_length, dtype=torch.float32), persistent=False)
|
| 88 |
+
self.num_features = 3 * (self.n_fft // 2 + 1)
|
| 89 |
+
|
| 90 |
+
def forward(self, audio: torch.Tensor) -> torch.Tensor:
|
| 91 |
+
total_pad = self.win_length - self.hop_length
|
| 92 |
+
audio = F.pad(audio.float(), (total_pad // 2, total_pad - total_pad // 2), mode="reflect")
|
| 93 |
+
spec = torch.stft(
|
| 94 |
+
audio,
|
| 95 |
+
n_fft=self.n_fft,
|
| 96 |
+
hop_length=self.hop_length,
|
| 97 |
+
win_length=self.win_length,
|
| 98 |
+
window=self.window.float(),
|
| 99 |
+
center=False,
|
| 100 |
+
return_complex=True,
|
| 101 |
+
)
|
| 102 |
+
magnitude = spec.abs()
|
| 103 |
+
phase_denominator = magnitude.clamp_min(1e-5)
|
| 104 |
+
return torch.cat((torch.log1p(magnitude), spec.real / phase_denominator, spec.imag / phase_denominator), dim=1)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class LiquidCodecFrontEnd(nn.Module):
|
| 108 |
+
def __init__(self, config: LiquidCodecConfig, num_features: int):
|
| 109 |
+
super().__init__()
|
| 110 |
+
self.frame_stack = config.stft_frame_stack
|
| 111 |
+
self.in_proj = nn.Conv1d(num_features, config.encoder_conv_dim, kernel_size=1)
|
| 112 |
+
self.conv = nn.Conv1d(config.encoder_conv_dim, config.encoder_conv_dim, kernel_size=3, padding=2)
|
| 113 |
+
self.out = nn.Conv1d(
|
| 114 |
+
config.encoder_conv_dim, config.encoder_dim, kernel_size=self.frame_stack, stride=self.frame_stack
|
| 115 |
+
)
|
| 116 |
+
self.act = nn.SiLU()
|
| 117 |
+
|
| 118 |
+
def forward(self, features: torch.Tensor) -> torch.Tensor:
|
| 119 |
+
hidden_states = self.act(self.in_proj(features.to(self.in_proj.weight.dtype)))
|
| 120 |
+
hidden_states = self.act(self.conv(hidden_states)[:, :, : hidden_states.shape[-1]])
|
| 121 |
+
return self.out(hidden_states)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class LiquidCodecLfm(nn.Module):
|
| 125 |
+
def __init__(self, config: LiquidCodecConfig, sliding_window: int | None = None):
|
| 126 |
+
super().__init__()
|
| 127 |
+
self.lfm = Lfm3Model(config.lfm_config(sliding_window))
|
| 128 |
+
self.lfm.register_module("embed_tokens", None)
|
| 129 |
+
# the projections run in the model dtype but the rotary tables stay in float32; a named hook keeps the
|
| 130 |
+
# module picklable
|
| 131 |
+
self.lfm.rotary_emb.register_forward_pre_hook(self._rotary_in_float32)
|
| 132 |
+
|
| 133 |
+
@staticmethod
|
| 134 |
+
def _rotary_in_float32(module, args):
|
| 135 |
+
return (args[0].float(), *args[1:])
|
| 136 |
+
|
| 137 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 138 |
+
return self.lfm(inputs_embeds=hidden_states, use_cache=False).last_hidden_state
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class LiquidCodecQuantizer(nn.Module):
|
| 142 |
+
def __init__(self, config: LiquidCodecConfig):
|
| 143 |
+
super().__init__()
|
| 144 |
+
levels = list(config.fsq_levels)
|
| 145 |
+
self.num_codebooks = config.n_q
|
| 146 |
+
self.codebook_dim = len(levels)
|
| 147 |
+
self.levels = nn.Buffer(torch.tensor(levels, dtype=torch.int32), persistent=False)
|
| 148 |
+
self.basis = nn.Buffer(
|
| 149 |
+
torch.tensor([math.prod(levels[:index]) for index in range(self.codebook_dim)], dtype=torch.int32),
|
| 150 |
+
persistent=False,
|
| 151 |
+
)
|
| 152 |
+
effective_dim = self.codebook_dim * self.num_codebooks
|
| 153 |
+
if config.quantizer_dim == effective_dim:
|
| 154 |
+
self.project_in = nn.Identity()
|
| 155 |
+
self.project_out = nn.Identity()
|
| 156 |
+
else:
|
| 157 |
+
self.project_in = nn.Linear(config.quantizer_dim, effective_dim)
|
| 158 |
+
self.project_out = nn.Linear(effective_dim, config.quantizer_dim)
|
| 159 |
+
|
| 160 |
+
def _bound(self, latents: torch.Tensor, eps: float = 1e-3) -> torch.Tensor:
|
| 161 |
+
half_levels = (self.levels - 1) * (1 + eps) / 2
|
| 162 |
+
offset = torch.where(self.levels % 2 == 0, 0.5, 0.0)
|
| 163 |
+
shift = torch.atanh(offset / half_levels)
|
| 164 |
+
return torch.tanh(latents + shift) * half_levels - offset
|
| 165 |
+
|
| 166 |
+
def encode(self, latents: torch.Tensor) -> torch.Tensor:
|
| 167 |
+
# the quantizer runs outside `forward`, so no `device_map` hook moves its inputs (here and in `decode`) to the
|
| 168 |
+
# device of its buffers
|
| 169 |
+
latents = self.project_in(latents.movedim(1, -1).contiguous()).float().to(self.levels.device)
|
| 170 |
+
if not is_tracing(latents):
|
| 171 |
+
# finite audio far above full scale overflows the float32 STFT, and NaN latents round to INT64_MIN codes
|
| 172 |
+
torch_compilable_check(
|
| 173 |
+
torch.isfinite(latents),
|
| 174 |
+
"The audio overflows the codec (non-finite latents); expected samples in [-1, 1]",
|
| 175 |
+
)
|
| 176 |
+
latents = latents.reshape(*latents.shape[:-1], self.num_codebooks, self.codebook_dim)
|
| 177 |
+
half_width = self.levels // 2
|
| 178 |
+
codes = self._bound(latents).round() / half_width
|
| 179 |
+
indices = ((codes * half_width + half_width) * self.basis).sum(-1).round().long()
|
| 180 |
+
return indices.transpose(1, 2)
|
| 181 |
+
|
| 182 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 183 |
+
indices = codes.to(self.basis.device).transpose(1, 2).unsqueeze(-1)
|
| 184 |
+
half_width = self.levels // 2
|
| 185 |
+
latents = ((indices // self.basis) % self.levels - half_width) / half_width
|
| 186 |
+
latents = latents.reshape(*latents.shape[:-2], -1)
|
| 187 |
+
if isinstance(self.project_out, nn.Linear):
|
| 188 |
+
latents = latents.to(self.project_out.weight.dtype)
|
| 189 |
+
return self.project_out(latents).movedim(-1, 1)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class LiquidCodecISTFT(nn.Module):
|
| 193 |
+
def __init__(self, config: LiquidCodecConfig):
|
| 194 |
+
super().__init__()
|
| 195 |
+
self.n_fft = config.stft_n_fft
|
| 196 |
+
self.hop_length = config.stft_hop_length
|
| 197 |
+
self.win_length = config.stft_win_length
|
| 198 |
+
self.window = nn.Buffer(torch.hann_window(self.win_length, dtype=torch.float32))
|
| 199 |
+
|
| 200 |
+
def forward(self, spec: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 201 |
+
num_frames = spec.shape[-1]
|
| 202 |
+
frames = torch.fft.irfft(spec, self.n_fft, dim=1, norm="backward") * self.window[None, :, None]
|
| 203 |
+
output_size = (num_frames - 1) * self.hop_length + self.win_length
|
| 204 |
+
fold = {"output_size": (1, output_size), "kernel_size": (1, self.win_length), "stride": (1, self.hop_length)}
|
| 205 |
+
audio = F.fold(frames, **fold)[:, 0, 0]
|
| 206 |
+
envelope = F.fold(self.window.square().expand(1, num_frames, -1).transpose(1, 2), **fold)[0, 0, 0]
|
| 207 |
+
return audio, envelope
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class LiquidCodecDecoder(nn.Module):
|
| 211 |
+
def __init__(self, config: LiquidCodecConfig):
|
| 212 |
+
super().__init__()
|
| 213 |
+
self.frame_stack = config.stft_frame_stack
|
| 214 |
+
self.lfm = LiquidCodecLfm(config, sliding_window=config.decoder_window).lfm
|
| 215 |
+
self.lin = nn.Linear(config.encoder_dim, 2 * (config.stft_n_fft // 2 + 1))
|
| 216 |
+
self.istft = LiquidCodecISTFT(config)
|
| 217 |
+
|
| 218 |
+
def forward(
|
| 219 |
+
self, embeddings: torch.Tensor, past_key_values: DynamicCache | None = None
|
| 220 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 221 |
+
hidden_states = self.lfm(
|
| 222 |
+
inputs_embeds=F.interpolate(
|
| 223 |
+
embeddings.mT, embeddings.shape[1] * self.frame_stack, mode="nearest-exact"
|
| 224 |
+
).mT,
|
| 225 |
+
past_key_values=past_key_values,
|
| 226 |
+
use_cache=past_key_values is not None,
|
| 227 |
+
).last_hidden_state
|
| 228 |
+
log_magnitude, angle = torch.chunk(self.lin(hidden_states).mT.contiguous(), 2, 1)
|
| 229 |
+
return self.istft(torch.polar(log_magnitude.float().exp(), angle.float()))
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
@auto_docstring(
|
| 233 |
+
custom_intro="The Liquid Codec neural audio codec: 24 kHz audio to 12 codebooks at 12.5 frames per second and back."
|
| 234 |
+
)
|
| 235 |
+
class LiquidCodecModel(PreTrainedAudioTokenizerBase):
|
| 236 |
+
config: LiquidCodecConfig
|
| 237 |
+
base_model_prefix = "codec"
|
| 238 |
+
main_input_name = "input_values"
|
| 239 |
+
# the analysis window is a non-persistent buffer cast in the forward; only the synthesis window is a weight
|
| 240 |
+
_keep_in_fp32_modules_strict = ["istft"]
|
| 241 |
+
_supports_flash_attn = False
|
| 242 |
+
_supports_sdpa = True
|
| 243 |
+
|
| 244 |
+
def __init__(self, config: LiquidCodecConfig):
|
| 245 |
+
super().__init__(config)
|
| 246 |
+
self.stft = LiquidCodecSTFT(config)
|
| 247 |
+
self.frontend = LiquidCodecFrontEnd(config, self.stft.num_features)
|
| 248 |
+
self.encoder = LiquidCodecLfm(config)
|
| 249 |
+
self.to_quantizer = nn.Linear(config.encoder_dim, config.quantizer_dim)
|
| 250 |
+
self.quantizer = LiquidCodecQuantizer(config)
|
| 251 |
+
self.decoder = LiquidCodecDecoder(config)
|
| 252 |
+
self.from_quantizer = nn.Linear(config.quantizer_dim, config.encoder_dim)
|
| 253 |
+
self.post_init()
|
| 254 |
+
|
| 255 |
+
@torch.no_grad()
|
| 256 |
+
def _init_weights(self, module):
|
| 257 |
+
super()._init_weights(module)
|
| 258 |
+
if isinstance(module, (LiquidCodecSTFT, LiquidCodecISTFT)):
|
| 259 |
+
init.copy_(module.window, torch.hann_window(module.win_length, dtype=torch.float32))
|
| 260 |
+
elif isinstance(module, LiquidCodecQuantizer):
|
| 261 |
+
levels = list(self.config.fsq_levels)
|
| 262 |
+
init.copy_(module.levels, torch.tensor(levels, dtype=torch.int32))
|
| 263 |
+
init.copy_(
|
| 264 |
+
module.basis,
|
| 265 |
+
torch.tensor([math.prod(levels[:index]) for index in range(len(levels))], dtype=torch.int32),
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
@can_return_tuple
|
| 269 |
+
def encode(self, input_values: torch.FloatTensor, *, return_dict: bool | None = None) -> LiquidCodecEncoderOutput:
|
| 270 |
+
r"""
|
| 271 |
+
Non-finite audio, or audio that overflows the codec, is refused eagerly; a compiled `encode` skips the check.
|
| 272 |
+
|
| 273 |
+
input_values (`torch.FloatTensor` of shape `(batch_size, num_samples)`):
|
| 274 |
+
24 kHz audio. The tail is zero-padded to a whole codec frame of 1920 samples.
|
| 275 |
+
return_dict (`bool`, *optional*):
|
| 276 |
+
Whether to return a [`~utils.ModelOutput`] instead of a plain tuple.
|
| 277 |
+
"""
|
| 278 |
+
if input_values.ndim != 2 or input_values.shape[-1] == 0:
|
| 279 |
+
raise ValueError(
|
| 280 |
+
f"Expected non-empty audio of shape (batch_size, num_samples), got {tuple(input_values.shape)}"
|
| 281 |
+
)
|
| 282 |
+
if not is_tracing(input_values):
|
| 283 |
+
# a non-finite sample would round to an INT64_MIN code that only fails far downstream; the tensor-valued
|
| 284 |
+
# check would break a `fullgraph` compile or a CUDA-graph capture
|
| 285 |
+
torch_compilable_check(torch.isfinite(input_values), "`input_values` must be finite")
|
| 286 |
+
frame_size = self.config.frame_size
|
| 287 |
+
num_samples = -(input_values.shape[-1] // -frame_size) * frame_size
|
| 288 |
+
input_values = F.pad(input_values, (0, num_samples - input_values.shape[-1]))
|
| 289 |
+
hidden_states = self.frontend(self.stft(input_values)).mT
|
| 290 |
+
latents = self.to_quantizer(self.encoder(hidden_states)).mT
|
| 291 |
+
return LiquidCodecEncoderOutput(audio_codes=self.quantizer.encode(latents))
|
| 292 |
+
|
| 293 |
+
@can_return_tuple
|
| 294 |
+
def decode(
|
| 295 |
+
self,
|
| 296 |
+
audio_codes: torch.LongTensor,
|
| 297 |
+
streaming_cache: LiquidCodecStreamingCache | None = None,
|
| 298 |
+
finalize: bool = True,
|
| 299 |
+
*,
|
| 300 |
+
return_dict: bool | None = None,
|
| 301 |
+
) -> LiquidCodecDecoderOutput:
|
| 302 |
+
r"""
|
| 303 |
+
audio_codes (`torch.LongTensor` of shape `(batch_size, num_codebooks, num_frames)`):
|
| 304 |
+
The codes to decode; every frame becomes 1920 samples.
|
| 305 |
+
streaming_cache ([`LiquidCodecStreamingCache`], *optional*):
|
| 306 |
+
The state carried between consecutive chunks of one stream. The chunks decode to the same audio as the
|
| 307 |
+
concatenated codes.
|
| 308 |
+
finalize (`bool`, *optional*, defaults to `True`):
|
| 309 |
+
Whether this is the last chunk of the stream. Until then the trailing samples that later frames still
|
| 310 |
+
contribute to are held back in the cache.
|
| 311 |
+
return_dict (`bool`, *optional*):
|
| 312 |
+
Whether to return a [`~utils.ModelOutput`] instead of a plain tuple.
|
| 313 |
+
"""
|
| 314 |
+
if audio_codes.ndim != 3 or audio_codes.shape[1] != self.config.n_q:
|
| 315 |
+
raise ValueError(
|
| 316 |
+
f"Expected codes of shape (batch_size, {self.config.n_q}, num_frames), got {tuple(audio_codes.shape)}"
|
| 317 |
+
)
|
| 318 |
+
torch_compilable_check(
|
| 319 |
+
(audio_codes >= 0) & (audio_codes < self.config.codebook_size),
|
| 320 |
+
f"Codes must be in [0, {self.config.codebook_size})",
|
| 321 |
+
)
|
| 322 |
+
if streaming_cache is not None and streaming_cache.finished:
|
| 323 |
+
raise ValueError("This stream is finalized; start with a new cache")
|
| 324 |
+
trim = (self.config.stft_win_length - self.config.stft_hop_length) // 2
|
| 325 |
+
tail = 2 * trim
|
| 326 |
+
batch_size = audio_codes.shape[0]
|
| 327 |
+
if not audio_codes.shape[-1] and not (finalize and streaming_cache is not None and streaming_cache.started):
|
| 328 |
+
# no frames and no held-back tail to flush: the overlap-add state must stay untouched
|
| 329 |
+
if streaming_cache is not None and finalize:
|
| 330 |
+
streaming_cache.finished = True
|
| 331 |
+
return LiquidCodecDecoderOutput(audio_values=audio_codes.new_zeros((batch_size, 0), dtype=torch.float32))
|
| 332 |
+
if audio_codes.shape[-1]:
|
| 333 |
+
# `quantizer.decode` returns float32 latents when its projections are `nn.Identity`
|
| 334 |
+
latents = self.quantizer.decode(audio_codes).mT.to(self.from_quantizer.weight.dtype)
|
| 335 |
+
embeddings = self.from_quantizer(latents)
|
| 336 |
+
past_key_values = None if streaming_cache is None else streaming_cache.past_key_values
|
| 337 |
+
audio, envelope = self.decoder(embeddings, past_key_values)
|
| 338 |
+
else:
|
| 339 |
+
audio = self.decoder.istft.window.new_zeros(batch_size, tail)
|
| 340 |
+
envelope = self.decoder.istft.window.new_zeros(tail)
|
| 341 |
+
if streaming_cache is None:
|
| 342 |
+
audio, envelope = audio[:, trim:-trim], envelope[trim:-trim]
|
| 343 |
+
else:
|
| 344 |
+
if streaming_cache.audio is not None:
|
| 345 |
+
audio = torch.cat([audio[:, :tail] + streaming_cache.audio, audio[:, tail:]], dim=1)
|
| 346 |
+
envelope = torch.cat([envelope[:tail] + streaming_cache.envelope, envelope[tail:]])
|
| 347 |
+
if not streaming_cache.started:
|
| 348 |
+
audio, envelope = audio[:, trim:], envelope[trim:]
|
| 349 |
+
streaming_cache.started = True
|
| 350 |
+
if finalize:
|
| 351 |
+
audio, envelope = audio[:, :-trim], envelope[:-trim]
|
| 352 |
+
streaming_cache.audio, streaming_cache.envelope = None, None
|
| 353 |
+
streaming_cache.finished = True
|
| 354 |
+
else:
|
| 355 |
+
streaming_cache.audio, streaming_cache.envelope = audio[:, -tail:], envelope[-tail:]
|
| 356 |
+
audio, envelope = audio[:, :-tail], envelope[:-tail]
|
| 357 |
+
return LiquidCodecDecoderOutput(audio_values=(audio / envelope).float())
|
| 358 |
+
|
| 359 |
+
@can_return_tuple
|
| 360 |
+
@auto_docstring
|
| 361 |
+
def forward(self, input_values: torch.FloatTensor) -> LiquidCodecOutput:
|
| 362 |
+
r"""
|
| 363 |
+
input_values (`torch.FloatTensor` of shape `(batch_size, num_samples)`):
|
| 364 |
+
24 kHz audio.
|
| 365 |
+
"""
|
| 366 |
+
audio_codes = self.encode(input_values, return_dict=True).audio_codes
|
| 367 |
+
audio = self.decode(audio_codes, return_dict=True).audio_values
|
| 368 |
+
return LiquidCodecOutput(audio_values=audio[..., : input_values.shape[-1]], audio_codes=audio_codes)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
__all__ = ["LiquidCodecModel", "LiquidCodecStreamingCache"]
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
register_checkpoint_conversion_mapping(
|
| 375 |
+
"liquid_codec",
|
| 376 |
+
[
|
| 377 |
+
WeightRenaming(source_patterns=r"^_frontend\.", target_patterns="frontend."),
|
| 378 |
+
WeightRenaming(source_patterns=r"^quantizer\.fsq\.", target_patterns="quantizer."),
|
| 379 |
+
],
|
| 380 |
+
overwrite=True,
|
| 381 |
+
)
|
processing_lfm3_omni.py
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import copy
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import torch
|
| 22 |
+
|
| 23 |
+
from transformers.audio_utils import AudioInput, make_list_of_audio
|
| 24 |
+
from transformers.feature_extraction_utils import BatchFeature
|
| 25 |
+
from transformers.image_utils import is_pil_image, is_valid_image, make_flat_list_of_images
|
| 26 |
+
from transformers.processing_utils import ProcessingKwargs, ProcessorMixin, VideosKwargs
|
| 27 |
+
from transformers.utils import auto_docstring, is_numpy_array, is_torch_tensor
|
| 28 |
+
from transformers.video_utils import is_valid_video, make_batched_metadata, make_batched_videos
|
| 29 |
+
|
| 30 |
+
from .modeling_lfm3_omni import Lfm3OmniFeatureExtractorKwargs
|
| 31 |
+
from .modeling_liquid_codec import LiquidCodecModel
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Lfm3OmniVideosKwargs(VideosKwargs, total=False):
|
| 35 |
+
"""
|
| 36 |
+
use_audio_in_video (`bool`, *optional*, defaults to `False`):
|
| 37 |
+
Whether each video comes with its soundtrack: the first audio clip after the video in the text (the one
|
| 38 |
+
`apply_chat_template(load_audio_from_video=True)` puts right behind the video) is interleaved after the
|
| 39 |
+
video's frame groups instead of standing in its own media span.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
use_audio_in_video: bool
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class Lfm3OmniProcessorKwargs(ProcessingKwargs, total=False):
|
| 46 |
+
audio_kwargs: Lfm3OmniFeatureExtractorKwargs
|
| 47 |
+
videos_kwargs: Lfm3OmniVideosKwargs
|
| 48 |
+
_defaults = {"videos_kwargs": {"return_metadata": True, "use_audio_in_video": False}}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# The prompt format of the training data: ChatML turns, one placeholder per clip, no thinking block
|
| 52 |
+
DEFAULT_CHAT_TEMPLATE = (
|
| 53 |
+
"{{- bos_token -}}"
|
| 54 |
+
"{%- for message in messages -%}"
|
| 55 |
+
"{{- '<|im_start|>' + message['role'] + '\\n' -}}"
|
| 56 |
+
"{%- if message['content'] is string -%}{{- message['content'] -}}"
|
| 57 |
+
"{%- else -%}{%- for content in message['content'] -%}"
|
| 58 |
+
"{%- if content['type'] == 'audio' -%}{{- '<|audio|>' -}}"
|
| 59 |
+
"{%- elif content['type'] == 'audio_out' -%}{{- '<|audio_out|>' -}}"
|
| 60 |
+
"{%- elif content['type'] == 'text' -%}{{- content['text'] -}}"
|
| 61 |
+
"{%- endif -%}{%- endfor -%}{%- endif -%}"
|
| 62 |
+
"{{- '<|im_end|>\\n' -}}"
|
| 63 |
+
"{%- endfor -%}"
|
| 64 |
+
"{%- if add_generation_prompt -%}{{- '<|im_start|>assistant\\n' -}}{%- endif -%}"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _media_per_prompt(media, is_item) -> list[int] | None:
|
| 69 |
+
# rows name the prompt that owns them while the expansion consumes media in flat order; a row is a list of whole
|
| 70 |
+
# items or one stacked batch, and a list of single frames is one video, not a row
|
| 71 |
+
if not isinstance(media, (list, tuple)):
|
| 72 |
+
return None
|
| 73 |
+
counts = []
|
| 74 |
+
for row in media:
|
| 75 |
+
if isinstance(row, (list, tuple)) and all(is_item(item) for item in row):
|
| 76 |
+
counts.append(len(row))
|
| 77 |
+
elif (is_numpy_array(row) or is_torch_tensor(row)) and row.ndim == 4 and (len(row) == 0 or is_item(row[0])):
|
| 78 |
+
counts.append(len(row))
|
| 79 |
+
else:
|
| 80 |
+
return None
|
| 81 |
+
return counts
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _check_vision_placeholder_counts(processor, prompts, images, videos) -> None:
|
| 85 |
+
for name, token, media, flatten, is_item in (
|
| 86 |
+
(
|
| 87 |
+
"images",
|
| 88 |
+
processor.image_token,
|
| 89 |
+
images,
|
| 90 |
+
make_flat_list_of_images,
|
| 91 |
+
lambda item: is_valid_image(item) and (is_pil_image(item) or item.ndim == 3),
|
| 92 |
+
),
|
| 93 |
+
(
|
| 94 |
+
"videos",
|
| 95 |
+
processor.video_token,
|
| 96 |
+
videos,
|
| 97 |
+
make_batched_videos,
|
| 98 |
+
lambda item: isinstance(item, str) or (not is_pil_image(item) and is_valid_video(item)),
|
| 99 |
+
),
|
| 100 |
+
):
|
| 101 |
+
num_placeholders = sum(prompt.count(token) for prompt in prompts)
|
| 102 |
+
num_media = 0 if media is None else len(flatten(media))
|
| 103 |
+
if num_placeholders != num_media:
|
| 104 |
+
raise ValueError(f"Found {num_placeholders} {token} tokens in the text but received {num_media} {name}")
|
| 105 |
+
# one prompt cannot take another's media
|
| 106 |
+
per_prompt = _media_per_prompt(media, is_item) if len(prompts) > 1 else None
|
| 107 |
+
in_text = [prompt.count(token) for prompt in prompts]
|
| 108 |
+
if per_prompt is not None and per_prompt != in_text:
|
| 109 |
+
raise ValueError(f"Found {in_text} {token} tokens per prompt but received {per_prompt} {name} per prompt")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _check_video_metadata_count(videos, video_metadata) -> None:
|
| 113 |
+
# the shared sampler pairs clips with metadata by `zip`, which drops the clips past the last entry
|
| 114 |
+
if video_metadata is None:
|
| 115 |
+
return
|
| 116 |
+
videos = make_batched_videos(videos)
|
| 117 |
+
entries = make_batched_metadata(videos, video_metadata) if video_metadata != [] else []
|
| 118 |
+
if len(entries) != len(videos):
|
| 119 |
+
raise ValueError(
|
| 120 |
+
f"Received {len(videos)} videos but {len(entries)} `video_metadata` entries; pass one entry per video"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
DEFAULT_OMNI_CHAT_TEMPLATE = DEFAULT_CHAT_TEMPLATE.replace(
|
| 125 |
+
"{%- if content['type'] == 'audio' -%}",
|
| 126 |
+
"{%- if content['type'] == 'image' -%}{{- '<|image|>' -}}"
|
| 127 |
+
"{%- elif content['type'] == 'video' -%}{{- '<|video|>' -}}"
|
| 128 |
+
"{%- elif content['type'] == 'audio' -%}",
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@auto_docstring
|
| 133 |
+
class Lfm3OmniProcessor(ProcessorMixin):
|
| 134 |
+
"""The LFM3-Audio processor extended with the LFM3-VL image and video processors: every image, video frame group
|
| 135 |
+
and input clip is expanded in place, wrapped in the media span tokens the Omni SFT data uses."""
|
| 136 |
+
|
| 137 |
+
attributes = ["image_processor", "feature_extractor", "tokenizer", "video_processor"]
|
| 138 |
+
tokenizer_class = "AutoTokenizer"
|
| 139 |
+
audio_tokenizer_class = "LiquidCodecModel"
|
| 140 |
+
valid_processor_kwargs = Lfm3OmniProcessorKwargs
|
| 141 |
+
|
| 142 |
+
def __init__(
|
| 143 |
+
self,
|
| 144 |
+
image_processor=None,
|
| 145 |
+
feature_extractor=None,
|
| 146 |
+
tokenizer=None,
|
| 147 |
+
video_processor=None,
|
| 148 |
+
audio_tokenizer=None,
|
| 149 |
+
chat_template=None,
|
| 150 |
+
audio_token="<|audio|>",
|
| 151 |
+
audio_out_token="<|audio_out|>",
|
| 152 |
+
image_token="<|image|>",
|
| 153 |
+
video_token="<|video|>",
|
| 154 |
+
vision_start_token="<|media_start|>",
|
| 155 |
+
vision_end_token="<|media_end|>",
|
| 156 |
+
vision_markers=True,
|
| 157 |
+
audio_markers=True,
|
| 158 |
+
**kwargs,
|
| 159 |
+
):
|
| 160 |
+
r"""
|
| 161 |
+
image_processor ([`Lfm3VlImageProcessor`]):
|
| 162 |
+
The image patch processor.
|
| 163 |
+
feature_extractor ([`Lfm3OmniFeatureExtractor`]):
|
| 164 |
+
The log-mel feature extractor.
|
| 165 |
+
tokenizer ([`PreTrainedTokenizerFast`]):
|
| 166 |
+
The LFM3 tokenizer.
|
| 167 |
+
video_processor ([`Lfm3VlVideoProcessor`], *optional*):
|
| 168 |
+
The video frame sampler and patch processor.
|
| 169 |
+
audio_tokenizer ([`LiquidCodecModel`], *optional*):
|
| 170 |
+
The codec for 24 kHz output audio.
|
| 171 |
+
chat_template (`str`, *optional*):
|
| 172 |
+
The conversation template.
|
| 173 |
+
audio_token (`str`, *optional*, defaults to `"<|audio|>"`):
|
| 174 |
+
The input audio placeholder.
|
| 175 |
+
audio_out_token (`str`, *optional*, defaults to `"<|audio_out|>"`):
|
| 176 |
+
The output audio placeholder.
|
| 177 |
+
image_token (`str`, *optional*, defaults to `"<|image|>"`):
|
| 178 |
+
The image placeholder.
|
| 179 |
+
video_token (`str`, *optional*, defaults to `"<|video|>"`):
|
| 180 |
+
The video placeholder.
|
| 181 |
+
vision_start_token (`str`, *optional*, defaults to `"<|media_start|>"`):
|
| 182 |
+
The token opening an image, a video and each of its timestamped frame groups.
|
| 183 |
+
vision_end_token (`str`, *optional*, defaults to `"<|media_end|>"`):
|
| 184 |
+
The token closing them.
|
| 185 |
+
vision_markers (`bool`, *optional*, defaults to `True`):
|
| 186 |
+
Whether images and videos are wrapped in the vision span tokens, as the Omni SFT data renders them.
|
| 187 |
+
audio_markers (`bool`, *optional*, defaults to `True`):
|
| 188 |
+
Whether input audio clips are wrapped in the same span tokens, as the Omni SFT data renders them.
|
| 189 |
+
"""
|
| 190 |
+
self.audio_token = audio_token
|
| 191 |
+
self.audio_out_token = audio_out_token
|
| 192 |
+
self.image_token = image_token
|
| 193 |
+
self.video_token = video_token
|
| 194 |
+
self.vision_start_token = vision_start_token
|
| 195 |
+
self.vision_end_token = vision_end_token
|
| 196 |
+
self.vision_markers = vision_markers
|
| 197 |
+
self.audio_markers = audio_markers
|
| 198 |
+
for token in (audio_token, audio_out_token, image_token, video_token):
|
| 199 |
+
if tokenizer.convert_tokens_to_ids(token) in (None, tokenizer.unk_token_id):
|
| 200 |
+
tokenizer.add_tokens([token], special_tokens=True)
|
| 201 |
+
# the span tokens exist in every LFM3 tokenizer under one of two names (`<|vision_*|>` in liquid7.1,
|
| 202 |
+
# `<|media_*|>` in liquid7.1_media) at the same ids; never mint new ones
|
| 203 |
+
for attribute, token, fallback in (
|
| 204 |
+
("vision_start_token", vision_start_token, "<|vision_start|>"),
|
| 205 |
+
("vision_end_token", vision_end_token, "<|vision_end|>"),
|
| 206 |
+
):
|
| 207 |
+
if tokenizer.convert_tokens_to_ids(token) in (None, tokenizer.unk_token_id):
|
| 208 |
+
if tokenizer.convert_tokens_to_ids(fallback) not in (None, tokenizer.unk_token_id):
|
| 209 |
+
setattr(self, attribute, fallback)
|
| 210 |
+
else:
|
| 211 |
+
tokenizer.add_tokens([token], special_tokens=True)
|
| 212 |
+
self.audio_token_id = tokenizer.convert_tokens_to_ids(audio_token)
|
| 213 |
+
self.audio_out_token_id = tokenizer.convert_tokens_to_ids(audio_out_token)
|
| 214 |
+
self.image_token_id = tokenizer.convert_tokens_to_ids(image_token)
|
| 215 |
+
self.video_token_id = tokenizer.convert_tokens_to_ids(video_token)
|
| 216 |
+
super().__init__(
|
| 217 |
+
image_processor,
|
| 218 |
+
feature_extractor,
|
| 219 |
+
tokenizer,
|
| 220 |
+
video_processor,
|
| 221 |
+
chat_template=chat_template if chat_template is not None else DEFAULT_OMNI_CHAT_TEMPLATE,
|
| 222 |
+
)
|
| 223 |
+
# a released transformers only accepts its own codec classes as `audio_tokenizer`
|
| 224 |
+
self.audio_tokenizer = audio_tokenizer
|
| 225 |
+
|
| 226 |
+
@classmethod
|
| 227 |
+
def from_pretrained(cls, pretrained_model_name_or_path, *args, audio_tokenizer=None, **kwargs):
|
| 228 |
+
# `ProcessorMixin` forwards unknown kwargs to the tokenizer loader, which would keep the codec object in the
|
| 229 |
+
# tokenizer config and break `save_pretrained`
|
| 230 |
+
processor = super().from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
|
| 231 |
+
if audio_tokenizer is not None:
|
| 232 |
+
processor.audio_tokenizer = audio_tokenizer
|
| 233 |
+
return processor
|
| 234 |
+
|
| 235 |
+
def to_dict(self):
|
| 236 |
+
# `ProcessorMixin.to_dict` deep-copies the attributes; the codec weights are saved on their own. A shallow
|
| 237 |
+
# clone drops the codec, so concurrent calls on `self` keep it.
|
| 238 |
+
clone = copy.copy(self)
|
| 239 |
+
codec = clone.__dict__.pop("audio_tokenizer", None)
|
| 240 |
+
output = ProcessorMixin.to_dict(clone)
|
| 241 |
+
if codec is not None:
|
| 242 |
+
output["audio_tokenizer_subfolder"] = "audio_tokenizer"
|
| 243 |
+
return output
|
| 244 |
+
|
| 245 |
+
def save_pretrained(self, save_directory, push_to_hub=False, **kwargs):
|
| 246 |
+
files = super().save_pretrained(save_directory, push_to_hub=False, **kwargs)
|
| 247 |
+
if getattr(self, "audio_tokenizer", None) is not None:
|
| 248 |
+
self.audio_tokenizer.save_pretrained(os.path.join(save_directory, "audio_tokenizer"))
|
| 249 |
+
if push_to_hub:
|
| 250 |
+
repo_id = kwargs.pop("repo_id", os.path.basename(os.path.abspath(save_directory)))
|
| 251 |
+
self.push_to_hub(repo_id, **kwargs)
|
| 252 |
+
return files
|
| 253 |
+
|
| 254 |
+
@classmethod
|
| 255 |
+
def get_processor_dict(cls, pretrained_model_name_or_path, **kwargs):
|
| 256 |
+
codec_kwargs = {
|
| 257 |
+
key: value
|
| 258 |
+
for key, value in kwargs.items()
|
| 259 |
+
if key in ("cache_dir", "force_download", "local_files_only", "token", "revision")
|
| 260 |
+
}
|
| 261 |
+
parent = kwargs.get("subfolder") or ""
|
| 262 |
+
processor_dict, kwargs = super().get_processor_dict(pretrained_model_name_or_path, **kwargs)
|
| 263 |
+
subfolder = processor_dict.pop("audio_tokenizer_subfolder", None)
|
| 264 |
+
if subfolder is not None:
|
| 265 |
+
if subfolder != "audio_tokenizer":
|
| 266 |
+
raise ValueError("Expected the bundled codec in the audio_tokenizer subfolder")
|
| 267 |
+
codec_class = LiquidCodecModel
|
| 268 |
+
processor_dict["audio_tokenizer"] = codec_class.from_pretrained(
|
| 269 |
+
pretrained_model_name_or_path, subfolder=os.path.join(parent, subfolder), **codec_kwargs
|
| 270 |
+
)
|
| 271 |
+
return processor_dict, kwargs
|
| 272 |
+
|
| 273 |
+
@auto_docstring
|
| 274 |
+
def __call__(
|
| 275 |
+
self,
|
| 276 |
+
text=None,
|
| 277 |
+
audio: AudioInput | None = None,
|
| 278 |
+
audio_out: AudioInput | None = None,
|
| 279 |
+
images=None,
|
| 280 |
+
videos=None,
|
| 281 |
+
**kwargs,
|
| 282 |
+
):
|
| 283 |
+
r"""
|
| 284 |
+
audio_out (`np.ndarray`, `torch.Tensor` or a list of them, *optional*):
|
| 285 |
+
24 kHz clips, one per `audio_out_token` in the text, encoded with the codec into `audio_out_codes`; the
|
| 286 |
+
placeholder is repeated once per frame plus once for the end-of-speech frame that closes every clip.
|
| 287 |
+
videos (`VideoInput`, *optional*):
|
| 288 |
+
One clip per `video_token`. With `use_audio_in_video=True` each video takes the first audio clip after it
|
| 289 |
+
as its soundtrack, interleaved after each timestamped frame group like the Omni SFT data; otherwise every
|
| 290 |
+
audio clip is its own media span.
|
| 291 |
+
"""
|
| 292 |
+
for name, media in (("audio", audio), ("audio_out", audio_out), ("images", images), ("videos", videos)):
|
| 293 |
+
if isinstance(media, (list, tuple)) and len(media) == 0:
|
| 294 |
+
raise ValueError(f"`{name}` must hold at least one item; pass `{name}=None` for prompts without one")
|
| 295 |
+
if text is None:
|
| 296 |
+
# media-only calls return the features alone, like the audio processor
|
| 297 |
+
if audio_out is not None:
|
| 298 |
+
raise ValueError("`audio_out` needs the text holding its placeholders")
|
| 299 |
+
return super().__call__(audio=audio, images=images, videos=videos, **kwargs)
|
| 300 |
+
single = isinstance(text, str)
|
| 301 |
+
caller = [text] if single else list(text)
|
| 302 |
+
stripped = [self._strip_spans(prompt) for prompt in caller]
|
| 303 |
+
prompts, offsets = [prompt for prompt, _ in stripped], [row for _, row in stripped]
|
| 304 |
+
audio_out_codes = None
|
| 305 |
+
if audio_out is not None:
|
| 306 |
+
prompts, audio_out_codes, audio_out_offsets = self._expand_audio_out(prompts, audio_out)
|
| 307 |
+
offsets = self._compose_replacement_offsets(caller, offsets, audio_out_offsets)
|
| 308 |
+
merged = self._merge_kwargs(
|
| 309 |
+
self.valid_processor_kwargs, tokenizer_init_kwargs=self.tokenizer.init_kwargs, **kwargs
|
| 310 |
+
)
|
| 311 |
+
return_tensors = merged["text_kwargs"].get("return_tensors")
|
| 312 |
+
if videos is not None:
|
| 313 |
+
outputs = self._call_with_videos(prompts, audio, images, videos, single, **kwargs)
|
| 314 |
+
else:
|
| 315 |
+
outputs = super().__call__(
|
| 316 |
+
text=prompts[0] if single else prompts, audio=audio, images=images, videos=videos, **kwargs
|
| 317 |
+
)
|
| 318 |
+
if "text_replacement_offsets" in outputs:
|
| 319 |
+
# the expansion saw the prompts after the span pairs were stripped and the speech placeholders repeated
|
| 320 |
+
outputs["text_replacement_offsets"] = self._compose_replacement_offsets(
|
| 321 |
+
caller, offsets, outputs["text_replacement_offsets"]
|
| 322 |
+
)
|
| 323 |
+
if audio_out_codes is not None:
|
| 324 |
+
self._check_audio_out_placeholders(outputs["input_ids"], audio_out_codes.shape[0])
|
| 325 |
+
outputs["audio_out_codes"] = self._as_return_tensors(audio_out_codes, return_tensors)
|
| 326 |
+
return outputs
|
| 327 |
+
|
| 328 |
+
def _check_audio_out_placeholders(self, input_ids, num_frames: int) -> None:
|
| 329 |
+
# truncation may have dropped placeholders the codes still cover
|
| 330 |
+
rows = input_ids.tolist() if hasattr(input_ids, "tolist") else input_ids
|
| 331 |
+
rows = [rows] if rows and isinstance(rows[0], int) else rows
|
| 332 |
+
num_placeholders = sum(row.count(self.audio_out_token_id) for row in rows)
|
| 333 |
+
if num_placeholders != num_frames:
|
| 334 |
+
raise ValueError(
|
| 335 |
+
f"Found {num_placeholders} {self.audio_out_token} tokens after tokenization for {num_frames} "
|
| 336 |
+
"speech frames; disable truncation or shorten the prompt"
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
@staticmethod
|
| 340 |
+
def _as_return_tensors(codes: torch.Tensor, return_tensors):
|
| 341 |
+
return BatchFeature({"codes": codes}, tensor_type=return_tensors)["codes"]
|
| 342 |
+
|
| 343 |
+
@staticmethod
|
| 344 |
+
def _rewrite_with_offsets(prompt: str, pattern: str, replace, kind: str) -> tuple[str, list[dict]]:
|
| 345 |
+
"""Rewrites `prompt` like `ProcessorMixin.get_text_with_replacements`, with the same offset records."""
|
| 346 |
+
pieces, row, last, offset = [], [], 0, 0
|
| 347 |
+
for match in re.finditer(pattern, prompt):
|
| 348 |
+
start, end = match.span()
|
| 349 |
+
replacement = replace(match)
|
| 350 |
+
pieces.extend((prompt[last:start], replacement))
|
| 351 |
+
row.append(
|
| 352 |
+
{
|
| 353 |
+
"type": kind,
|
| 354 |
+
"span": (start, end),
|
| 355 |
+
"new_span": (start + offset, start + offset + len(replacement)),
|
| 356 |
+
"text": match.group(),
|
| 357 |
+
"replacement": replacement,
|
| 358 |
+
}
|
| 359 |
+
)
|
| 360 |
+
offset += len(replacement) - (end - start)
|
| 361 |
+
last = end
|
| 362 |
+
pieces.append(prompt[last:])
|
| 363 |
+
return "".join(pieces), row
|
| 364 |
+
|
| 365 |
+
@staticmethod
|
| 366 |
+
def _compose_replacement_offsets(prompts: list[str], first: list[list[dict]], second) -> list[list[dict]]:
|
| 367 |
+
"""The records of two successive rewrites (the caller's text A to B, then B to C) as one rewrite of A: an entry
|
| 368 |
+
of the second that covers entries of the first absorbs them, so every entry spans the caller's text."""
|
| 369 |
+
second = second or [[] for _ in prompts]
|
| 370 |
+
composed = []
|
| 371 |
+
for prompt, row_first, row_second in zip(prompts, first, second):
|
| 372 |
+
|
| 373 |
+
def to_first(position, row_first=row_first):
|
| 374 |
+
return position - sum(
|
| 375 |
+
len(r["replacement"]) - (r["span"][1] - r["span"][0])
|
| 376 |
+
for r in row_first
|
| 377 |
+
if r["new_span"][1] <= position
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
mapped = []
|
| 381 |
+
for entry in row_second:
|
| 382 |
+
start, end = to_first(entry["span"][0]), to_first(entry["span"][1])
|
| 383 |
+
mapped.append({**entry, "span": (start, end), "text": prompt[start:end]})
|
| 384 |
+
kept = [
|
| 385 |
+
r
|
| 386 |
+
for r in row_first
|
| 387 |
+
if not any(m["span"][0] <= r["span"][0] and r["span"][1] <= m["span"][1] for m in mapped)
|
| 388 |
+
]
|
| 389 |
+
row, offset = [], 0
|
| 390 |
+
for entry in sorted(kept + mapped, key=lambda r: r["span"]):
|
| 391 |
+
start, end = entry["span"]
|
| 392 |
+
row.append({**entry, "new_span": (start + offset, start + offset + len(entry["replacement"]))})
|
| 393 |
+
offset += len(entry["replacement"]) - (end - start)
|
| 394 |
+
composed.append(row)
|
| 395 |
+
return composed
|
| 396 |
+
|
| 397 |
+
def _expand_audio_out(
|
| 398 |
+
self, prompts: list[str], audio_out: AudioInput
|
| 399 |
+
) -> tuple[list[str], torch.Tensor, list[list[dict]]]:
|
| 400 |
+
if getattr(self, "audio_tokenizer", None) is None:
|
| 401 |
+
raise ValueError("`audio_out` requires the processor to hold the codec as `audio_tokenizer`")
|
| 402 |
+
clips = make_list_of_audio(audio_out)
|
| 403 |
+
if sum(prompt.count(self.audio_out_token) for prompt in prompts) != len(clips):
|
| 404 |
+
raise ValueError(f"The number of {self.audio_out_token} tokens in the text must match the number of clips")
|
| 405 |
+
codes = [self.encode_audio(clip) for clip in clips]
|
| 406 |
+
# the model ends speech with a frame of end-of-speech codes before the next text token; the prompt and the
|
| 407 |
+
# training targets carry it the same way `generate(return_audio_eos=True)` does
|
| 408 |
+
eos_frame = torch.full((1, codes[0].shape[1]), self.audio_eos_code, dtype=codes[0].dtype)
|
| 409 |
+
codes = [torch.cat((frames, eos_frame)) for frames in codes]
|
| 410 |
+
frame_counts = iter(code.shape[0] for code in codes)
|
| 411 |
+
expanded, offsets = [], []
|
| 412 |
+
for prompt in prompts:
|
| 413 |
+
prompt, row = self._rewrite_with_offsets(
|
| 414 |
+
prompt,
|
| 415 |
+
re.escape(self.audio_out_token),
|
| 416 |
+
lambda match: self.audio_out_token * next(frame_counts),
|
| 417 |
+
"audio_out",
|
| 418 |
+
)
|
| 419 |
+
expanded.append(prompt)
|
| 420 |
+
offsets.append(row)
|
| 421 |
+
return expanded, torch.cat(codes), offsets
|
| 422 |
+
|
| 423 |
+
@property
|
| 424 |
+
def audio_eos_code(self) -> int:
|
| 425 |
+
"""The end-of-speech code: the first id after the codec's vocabulary."""
|
| 426 |
+
return math.prod(self.audio_tokenizer.config.fsq_levels)
|
| 427 |
+
|
| 428 |
+
@torch.no_grad()
|
| 429 |
+
def encode_audio(self, audio) -> torch.Tensor:
|
| 430 |
+
codec = self.audio_tokenizer
|
| 431 |
+
waveform = torch.as_tensor(audio if torch.is_tensor(audio) else np.asarray(audio), dtype=torch.float32)
|
| 432 |
+
# the quantizer rounds non-finite values to INT64_MIN, which would surface as codes
|
| 433 |
+
if waveform.ndim != 1 or not torch.isfinite(waveform).all():
|
| 434 |
+
raise ValueError("`audio_out` clips must be mono waveforms of finite samples")
|
| 435 |
+
# an offloaded codec keeps some weights on `meta`: send the clip to a device that holds weights, and the hooks
|
| 436 |
+
# move it on from there
|
| 437 |
+
device = next(
|
| 438 |
+
(param.device for param in codec.parameters() if param.device.type != "meta"), torch.device("cpu")
|
| 439 |
+
)
|
| 440 |
+
# the codec's return type follows its config's `return_dict`
|
| 441 |
+
codes = codec.encode(waveform.to(device)[None], return_dict=True).audio_codes[0].T.cpu()
|
| 442 |
+
# a compiled codec skips its latent check, and a non-finite latent rounds to INT64_MIN
|
| 443 |
+
if codes.numel() and not 0 <= int(codes.min()) <= int(codes.max()) < self.audio_eos_code:
|
| 444 |
+
raise ValueError("The audio overflows the codec; expected `audio_out` samples in [-1, 1]")
|
| 445 |
+
return codes
|
| 446 |
+
|
| 447 |
+
def validate_inputs(self, images=None, text=None, videos=None, audio=None, **kwargs):
|
| 448 |
+
super().validate_inputs(images=images, text=text, videos=videos, audio=audio, **kwargs)
|
| 449 |
+
if text is None:
|
| 450 |
+
return
|
| 451 |
+
prompts = [text] if isinstance(text, str) else text
|
| 452 |
+
num_placeholders = sum(prompt.count(self.audio_token) for prompt in prompts)
|
| 453 |
+
num_clips = 0 if audio is None else len(make_list_of_audio(audio))
|
| 454 |
+
if num_placeholders != num_clips:
|
| 455 |
+
raise ValueError(
|
| 456 |
+
f"Found {num_placeholders} {self.audio_token} tokens in the text but received {num_clips} clips"
|
| 457 |
+
)
|
| 458 |
+
if text is not None:
|
| 459 |
+
_check_vision_placeholder_counts(self, [text] if isinstance(text, str) else text, images, videos)
|
| 460 |
+
|
| 461 |
+
def _process_audio(self, audio: AudioInput, **kwargs):
|
| 462 |
+
# the backend only serves `apply_chat_template`'s audio loading
|
| 463 |
+
kwargs.pop("load_audio_backend", None)
|
| 464 |
+
audio_inputs = self.feature_extractor(audio, **kwargs)
|
| 465 |
+
lengths = np.asarray(audio_inputs["feature_attention_mask"]).sum(-1).tolist()
|
| 466 |
+
audio_inputs["num_audio_tokens"] = [
|
| 467 |
+
self.feature_extractor.num_encoder_tokens(int(length)) for length in lengths
|
| 468 |
+
]
|
| 469 |
+
audio_replacements = [self.replace_audio_token(audio_inputs, audio_idx=idx) for idx in range(len(lengths))]
|
| 470 |
+
return audio_inputs, audio_replacements
|
| 471 |
+
|
| 472 |
+
def replace_audio_token(self, audio_inputs: dict, audio_idx: int, **kwargs) -> str:
|
| 473 |
+
return self._wrap(self.audio_token * audio_inputs["num_audio_tokens"][audio_idx], self.audio_markers)
|
| 474 |
+
|
| 475 |
+
@property
|
| 476 |
+
def unused_input_names(self) -> list[str]:
|
| 477 |
+
return ["num_audio_tokens"]
|
| 478 |
+
|
| 479 |
+
@property
|
| 480 |
+
def model_input_names(self):
|
| 481 |
+
return list(
|
| 482 |
+
dict.fromkeys(
|
| 483 |
+
self.tokenizer.model_input_names
|
| 484 |
+
+ self.feature_extractor.model_input_names
|
| 485 |
+
+ self.image_processor.model_input_names
|
| 486 |
+
+ (self.video_processor.model_input_names if self.video_processor is not None else [])
|
| 487 |
+
+ ["audio_out_codes"]
|
| 488 |
+
)
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
def _wrap(self, placeholders: str, markers: bool) -> str:
|
| 492 |
+
return self.vision_start_token + placeholders + self.vision_end_token if markers else placeholders
|
| 493 |
+
|
| 494 |
+
def _strip_spans(self, prompt: str) -> tuple[str, list[dict]]:
|
| 495 |
+
# a chat template may already wrap a placeholder in the span pair; the expansion adds exactly one pair
|
| 496 |
+
wrapped = {
|
| 497 |
+
self.vision_start_token + token + self.vision_end_token: token
|
| 498 |
+
for token in (self.image_token, self.video_token, self.audio_token)
|
| 499 |
+
}
|
| 500 |
+
return self._rewrite_with_offsets(
|
| 501 |
+
prompt, "|".join(map(re.escape, wrapped)), lambda match: wrapped[match.group()], "span"
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
def apply_chat_template(
|
| 505 |
+
self,
|
| 506 |
+
conversation,
|
| 507 |
+
chat_template=None,
|
| 508 |
+
tools=None,
|
| 509 |
+
documents=None,
|
| 510 |
+
add_generation_prompt=False,
|
| 511 |
+
continue_final_message=False,
|
| 512 |
+
return_assistant_tokens_mask=False,
|
| 513 |
+
tokenize=False,
|
| 514 |
+
return_tensors=None,
|
| 515 |
+
return_dict=False,
|
| 516 |
+
load_audio_from_video=False,
|
| 517 |
+
processor_kwargs=None,
|
| 518 |
+
**kwargs,
|
| 519 |
+
):
|
| 520 |
+
"""With `load_audio_from_video=True`, each video's soundtrack becomes the audio part right behind it, which
|
| 521 |
+
`use_audio_in_video` pairs with the video; standalone clips are kept (the shared implementation drops them)."""
|
| 522 |
+
if load_audio_from_video and tokenize:
|
| 523 |
+
load_audio_from_video = False
|
| 524 |
+
conversation = self._with_soundtrack_parts(conversation)
|
| 525 |
+
return super().apply_chat_template(
|
| 526 |
+
conversation,
|
| 527 |
+
chat_template,
|
| 528 |
+
tools=tools,
|
| 529 |
+
documents=documents,
|
| 530 |
+
add_generation_prompt=add_generation_prompt,
|
| 531 |
+
continue_final_message=continue_final_message,
|
| 532 |
+
return_assistant_tokens_mask=return_assistant_tokens_mask,
|
| 533 |
+
tokenize=tokenize,
|
| 534 |
+
return_tensors=return_tensors,
|
| 535 |
+
return_dict=return_dict,
|
| 536 |
+
load_audio_from_video=load_audio_from_video,
|
| 537 |
+
processor_kwargs=processor_kwargs,
|
| 538 |
+
**kwargs,
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
@staticmethod
|
| 542 |
+
def _with_soundtrack_parts(conversation):
|
| 543 |
+
if not conversation:
|
| 544 |
+
return conversation
|
| 545 |
+
batched = isinstance(conversation[0], (list, tuple))
|
| 546 |
+
rewritten = []
|
| 547 |
+
for messages in conversation if batched else [conversation]:
|
| 548 |
+
rewritten.append([])
|
| 549 |
+
for message in messages:
|
| 550 |
+
content = message.get("content")
|
| 551 |
+
if isinstance(content, list):
|
| 552 |
+
parts = []
|
| 553 |
+
for part in content:
|
| 554 |
+
parts.append(part)
|
| 555 |
+
source = next((part[key] for key in ("video", "url", "path") if key in part), None)
|
| 556 |
+
if part.get("type") == "video" and source is not None:
|
| 557 |
+
parts.append({"type": "audio", "audio": source})
|
| 558 |
+
message = {**message, "content": parts}
|
| 559 |
+
rewritten[-1].append(message)
|
| 560 |
+
return rewritten if batched else rewritten[0]
|
| 561 |
+
|
| 562 |
+
def _attach_soundtracks(self, prompt: str) -> tuple[str, list[dict]]:
|
| 563 |
+
"""`use_audio_in_video`: each video takes the first audio placeholder after it as its soundtrack, moved right
|
| 564 |
+
behind the video; returns the prompt and the offset records of the moves."""
|
| 565 |
+
row, taken = [], set()
|
| 566 |
+
audio = [match.start() for match in re.finditer(re.escape(self.audio_token), prompt)]
|
| 567 |
+
for video in re.finditer(re.escape(self.video_token), prompt):
|
| 568 |
+
position = next((p for p in audio if p >= video.end() and p not in taken), None)
|
| 569 |
+
if position is None:
|
| 570 |
+
raise ValueError("`use_audio_in_video` needs an audio clip after each video")
|
| 571 |
+
taken.add(position)
|
| 572 |
+
row.append(
|
| 573 |
+
{"type": "audio", "span": (video.end(), video.end()), "text": "", "replacement": self.audio_token}
|
| 574 |
+
)
|
| 575 |
+
row.append(
|
| 576 |
+
{
|
| 577 |
+
"type": "audio",
|
| 578 |
+
"span": (position, position + len(self.audio_token)),
|
| 579 |
+
"text": self.audio_token,
|
| 580 |
+
"replacement": "",
|
| 581 |
+
}
|
| 582 |
+
)
|
| 583 |
+
row.sort(key=lambda r: r["span"])
|
| 584 |
+
pieces, last, offset = [], 0, 0
|
| 585 |
+
for entry in row:
|
| 586 |
+
start, end = entry["span"]
|
| 587 |
+
pieces.extend((prompt[last:start], entry["replacement"]))
|
| 588 |
+
entry["new_span"] = (start + offset, start + offset + len(entry["replacement"]))
|
| 589 |
+
offset += len(entry["replacement"]) - (end - start)
|
| 590 |
+
last = end
|
| 591 |
+
pieces.append(prompt[last:])
|
| 592 |
+
return "".join(pieces), row
|
| 593 |
+
|
| 594 |
+
def _call_with_videos(self, prompts, audio, images, videos, single, **kwargs):
|
| 595 |
+
"""Expands the placeholders itself: a soundtrack's encoder tokens are split across its video's frame groups,
|
| 596 |
+
which the generic one-clip-one-run expansion cannot do."""
|
| 597 |
+
images, prompts, videos, audio = self.prepare_inputs_layout(
|
| 598 |
+
images=images, text=prompts, videos=videos, audio=audio, **kwargs
|
| 599 |
+
)
|
| 600 |
+
self.validate_inputs(images=images, text=prompts, videos=videos, audio=audio, **kwargs)
|
| 601 |
+
merged = self._merge_kwargs(
|
| 602 |
+
self.valid_processor_kwargs, tokenizer_init_kwargs=self.tokenizer.init_kwargs, **kwargs
|
| 603 |
+
)
|
| 604 |
+
return_tensors = merged["text_kwargs"].pop("return_tensors", None)
|
| 605 |
+
return_mm_token_type_ids = merged["text_kwargs"].pop("return_mm_token_type_ids", False)
|
| 606 |
+
return_text_replacement_offsets = merged["text_kwargs"].pop("return_text_replacement_offsets", False)
|
| 607 |
+
# Timestamps always need metadata internally, independently of whether the caller wants it returned.
|
| 608 |
+
return_metadata = kwargs.get("return_metadata", kwargs.get("videos_kwargs", {}).get("return_metadata", False))
|
| 609 |
+
merged["videos_kwargs"]["return_metadata"] = True
|
| 610 |
+
use_audio_in_video = merged["videos_kwargs"].pop("use_audio_in_video")
|
| 611 |
+
unattached, attach_offsets = prompts, [[] for _ in prompts]
|
| 612 |
+
if use_audio_in_video:
|
| 613 |
+
attached = [self._attach_soundtracks(prompt) for prompt in prompts]
|
| 614 |
+
prompts, attach_offsets = [prompt for prompt, _ in attached], [row for _, row in attached]
|
| 615 |
+
outputs = {}
|
| 616 |
+
clips = make_list_of_audio(audio) if audio is not None else []
|
| 617 |
+
videos = make_batched_videos(videos)
|
| 618 |
+
self._check_video_clock(videos, merged["videos_kwargs"].get("video_metadata"))
|
| 619 |
+
image_inputs = self.image_processor(images=images, **merged["images_kwargs"]) if images is not None else None
|
| 620 |
+
if image_inputs is not None:
|
| 621 |
+
outputs.update(image_inputs)
|
| 622 |
+
if clips:
|
| 623 |
+
# the decoding backend is a loader option, not a feature-extractor one (the shared path drops it too)
|
| 624 |
+
merged["audio_kwargs"].pop("load_audio_backend", None)
|
| 625 |
+
audio_inputs = self.feature_extractor(clips, **merged["audio_kwargs"])
|
| 626 |
+
lengths = np.asarray(audio_inputs["feature_attention_mask"]).sum(-1).tolist()
|
| 627 |
+
audio_tokens = [self.feature_extractor.num_encoder_tokens(int(length)) for length in lengths]
|
| 628 |
+
outputs.update(audio_inputs)
|
| 629 |
+
video_inputs = self.video_processor(videos=videos, **merged["videos_kwargs"])
|
| 630 |
+
outputs.update(
|
| 631 |
+
{key: value for key, value in video_inputs.items() if key != "video_metadata" or return_metadata}
|
| 632 |
+
)
|
| 633 |
+
video = self.video_token + (self.audio_token if use_audio_in_video else "")
|
| 634 |
+
pattern = re.compile("|".join(map(re.escape, (video, self.image_token, self.audio_token))))
|
| 635 |
+
counters = {"image": 0, "video": 0, "audio": 0}
|
| 636 |
+
|
| 637 |
+
def expand(match):
|
| 638 |
+
token = match.group(0)
|
| 639 |
+
if token == self.image_token:
|
| 640 |
+
text = self.replace_image_token(image_inputs, counters["image"])
|
| 641 |
+
counters["image"] += 1
|
| 642 |
+
return text
|
| 643 |
+
if token == self.audio_token:
|
| 644 |
+
text = self._wrap(self.audio_token * audio_tokens[counters["audio"]], self.audio_markers)
|
| 645 |
+
counters["audio"] += 1
|
| 646 |
+
return text
|
| 647 |
+
soundtrack = None
|
| 648 |
+
if token != self.video_token: # the pair `_attach_soundtracks` made
|
| 649 |
+
soundtrack = audio_tokens[counters["audio"]]
|
| 650 |
+
counters["audio"] += 1
|
| 651 |
+
text = self._video_expansion(video_inputs, counters["video"], soundtrack)
|
| 652 |
+
counters["video"] += 1
|
| 653 |
+
return text
|
| 654 |
+
|
| 655 |
+
expanded, batch_offsets = [], []
|
| 656 |
+
for prompt in prompts:
|
| 657 |
+
pieces, offsets = [], []
|
| 658 |
+
last, offset = 0, 0
|
| 659 |
+
for match in pattern.finditer(prompt):
|
| 660 |
+
start, end = match.span()
|
| 661 |
+
replacement = expand(match)
|
| 662 |
+
pieces.extend((prompt[last:start], replacement))
|
| 663 |
+
token = match.group(0)
|
| 664 |
+
offsets.append(
|
| 665 |
+
{
|
| 666 |
+
"type": "video"
|
| 667 |
+
if token.startswith(self.video_token)
|
| 668 |
+
else "image"
|
| 669 |
+
if token == self.image_token
|
| 670 |
+
else "audio",
|
| 671 |
+
"span": (start, end),
|
| 672 |
+
"new_span": (start + offset, start + offset + len(replacement)),
|
| 673 |
+
"text": token,
|
| 674 |
+
"replacement": replacement,
|
| 675 |
+
}
|
| 676 |
+
)
|
| 677 |
+
offset += len(replacement) - (end - start)
|
| 678 |
+
last = end
|
| 679 |
+
pieces.append(prompt[last:])
|
| 680 |
+
expanded.append("".join(pieces))
|
| 681 |
+
batch_offsets.append(offsets)
|
| 682 |
+
text_inputs = self.tokenizer(expanded, **merged["text_kwargs"])
|
| 683 |
+
self._check_special_mm_tokens(expanded, text_inputs, modalities=["image", "video", "audio"])
|
| 684 |
+
if return_text_replacement_offsets:
|
| 685 |
+
text_inputs["text_replacement_offsets"] = self._compose_replacement_offsets(
|
| 686 |
+
unattached, attach_offsets, batch_offsets
|
| 687 |
+
)
|
| 688 |
+
if return_mm_token_type_ids:
|
| 689 |
+
text_inputs["mm_token_type_ids"] = self.create_mm_token_type_ids(text_inputs["input_ids"])
|
| 690 |
+
outputs.update(text_inputs)
|
| 691 |
+
return BatchFeature(outputs, tensor_type=return_tensors, skip_tensor_conversion=self.skip_tensor_conversion)
|
| 692 |
+
|
| 693 |
+
def _video_expansion(self, video_inputs: dict, video_idx: int, soundtrack_tokens: int | None = None) -> str:
|
| 694 |
+
grid = video_inputs["video_grid_thw"][video_idx]
|
| 695 |
+
patch_tokens = int(grid[1:].prod() // self.video_processor.merge_size**2)
|
| 696 |
+
metadata = video_inputs["video_metadata"][video_idx]
|
| 697 |
+
if metadata.frames_indices is None or not (
|
| 698 |
+
metadata.fps is not None and math.isfinite(metadata.fps) and metadata.fps > 0
|
| 699 |
+
):
|
| 700 |
+
raise ValueError(
|
| 701 |
+
"LFM3-Omni prompts carry frame timestamps: pass videos with `video_metadata` holding the sampled frame "
|
| 702 |
+
"indices and a positive fps"
|
| 703 |
+
)
|
| 704 |
+
# a released transformers reads torchcodec's scanned average fps, which carries float noise
|
| 705 |
+
# (30.000000000000004) where the fork reads the container header
|
| 706 |
+
metadata.fps = round(metadata.fps, 6)
|
| 707 |
+
temporal = self.video_processor.temporal_patch_size
|
| 708 |
+
timestamps = self._calculate_timestamps(metadata.frames_indices, metadata.fps, temporal)[: int(grid[0])]
|
| 709 |
+
bounds = None
|
| 710 |
+
if soundtrack_tokens is not None:
|
| 711 |
+
# the soundtrack's encoder tokens are split at each frame group's first frame
|
| 712 |
+
starts = [index / metadata.fps for index in list(metadata.frames_indices)[::temporal]][: int(grid[0])]
|
| 713 |
+
seconds_per_token = (
|
| 714 |
+
self.feature_extractor.hop_length
|
| 715 |
+
* self.feature_extractor.encoder_stride
|
| 716 |
+
/ self.feature_extractor.sampling_rate
|
| 717 |
+
)
|
| 718 |
+
bounds = (
|
| 719 |
+
[0]
|
| 720 |
+
+ [min(soundtrack_tokens, round(start / seconds_per_token)) for start in starts[1:]]
|
| 721 |
+
+ [soundtrack_tokens]
|
| 722 |
+
)
|
| 723 |
+
bounds = [max(bounds[: i + 1]) for i in range(len(bounds))]
|
| 724 |
+
groups = ""
|
| 725 |
+
for index, timestamp in enumerate(timestamps):
|
| 726 |
+
groups += f"<{timestamp:.1f} seconds>" + self._wrap(self.video_token * patch_tokens, self.vision_markers)
|
| 727 |
+
if bounds is not None:
|
| 728 |
+
groups += self.audio_token * (bounds[index + 1] - bounds[index])
|
| 729 |
+
return self._wrap(groups, self.vision_markers)
|
| 730 |
+
|
| 731 |
+
def _check_video_clock(self, videos, video_metadata) -> None:
|
| 732 |
+
_check_video_metadata_count(videos, video_metadata)
|
| 733 |
+
if video_metadata is None:
|
| 734 |
+
if any(not isinstance(video, str) for video in make_batched_videos(videos)):
|
| 735 |
+
# decoded frames carry no clock: without metadata the timestamps would silently assume 24 fps
|
| 736 |
+
raise ValueError(
|
| 737 |
+
"LFM3-Omni prompts carry frame timestamps: pass decoded frames with `video_metadata` holding the "
|
| 738 |
+
"clip's fps and total_num_frames (a path or URL is decoded with its own metadata)"
|
| 739 |
+
)
|
| 740 |
+
# the shared sampler writes a 24 fps default into metadata lacking a clock before the expansion runs
|
| 741 |
+
elif any(metadata.fps is None for metadata in make_batched_metadata(videos, video_metadata)):
|
| 742 |
+
raise ValueError(
|
| 743 |
+
"LFM3-Omni prompts carry frame timestamps: `video_metadata` must hold the clip's fps "
|
| 744 |
+
"(a path or URL is decoded with its own metadata)"
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
def _process_videos(self, videos, **kwargs):
|
| 748 |
+
# only media-only calls reach the shared path (prompts with videos take `_call_with_videos`): the pairing
|
| 749 |
+
# flag is not a video-processor argument and no placeholder text is needed
|
| 750 |
+
kwargs.pop("use_audio_in_video", None)
|
| 751 |
+
self._check_video_clock(videos, kwargs.get("video_metadata"))
|
| 752 |
+
return self.video_processor(videos, **kwargs), []
|
| 753 |
+
|
| 754 |
+
def replace_image_token(self, image_inputs, image_idx, **kwargs):
|
| 755 |
+
count = image_inputs["image_grid_thw"][image_idx].prod() // self.image_processor.merge_size**2
|
| 756 |
+
return self._wrap(self.image_token * int(count), self.vision_markers)
|
| 757 |
+
|
| 758 |
+
@staticmethod
|
| 759 |
+
def _calculate_timestamps(indices, video_fps: float, temporal_patch_size: int) -> list[float]:
|
| 760 |
+
indices = list(indices)
|
| 761 |
+
if pad := -len(indices) % temporal_patch_size:
|
| 762 |
+
indices.extend([indices[-1]] * pad)
|
| 763 |
+
return [
|
| 764 |
+
(indices[i] + indices[i + temporal_patch_size - 1]) / (2 * video_fps)
|
| 765 |
+
for i in range(0, len(indices), temporal_patch_size)
|
| 766 |
+
]
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
__all__ = ["Lfm3OmniProcessor"]
|
processor_config.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_markers": true,
|
| 3 |
+
"audio_out_token": "<|audio_out|>",
|
| 4 |
+
"audio_token": "<|audio|>",
|
| 5 |
+
"audio_tokenizer_subfolder": "audio_tokenizer",
|
| 6 |
+
"auto_map": {
|
| 7 |
+
"AutoProcessor": "processing_lfm3_omni.Lfm3OmniProcessor"
|
| 8 |
+
},
|
| 9 |
+
"feature_extractor": {
|
| 10 |
+
"auto_map": {
|
| 11 |
+
"AutoFeatureExtractor": "feature_extraction_lfm3_omni.Lfm3OmniFeatureExtractor",
|
| 12 |
+
"AutoProcessor": "processing_lfm3_omni.Lfm3OmniProcessor"
|
| 13 |
+
},
|
| 14 |
+
"encoder_stride": 8,
|
| 15 |
+
"feature_extractor_type": "Lfm3OmniFeatureExtractor",
|
| 16 |
+
"feature_size": 128,
|
| 17 |
+
"hop_length": 160,
|
| 18 |
+
"log_floor": -12.0,
|
| 19 |
+
"n_fft": 400,
|
| 20 |
+
"normalization_divisor": 4.0,
|
| 21 |
+
"normalization_offset": 4.0,
|
| 22 |
+
"padding_side": "right",
|
| 23 |
+
"padding_value": 0.0,
|
| 24 |
+
"return_attention_mask": true,
|
| 25 |
+
"sampling_rate": 16000,
|
| 26 |
+
"win_length": 400
|
| 27 |
+
},
|
| 28 |
+
"image_processor": {
|
| 29 |
+
"auto_map": {
|
| 30 |
+
"AutoImageProcessor": "image_processing_lfm3_vl.Lfm3VlImageProcessor",
|
| 31 |
+
"AutoProcessor": "processing_lfm3_omni.Lfm3OmniProcessor"
|
| 32 |
+
},
|
| 33 |
+
"do_convert_rgb": true,
|
| 34 |
+
"do_normalize": true,
|
| 35 |
+
"do_rescale": true,
|
| 36 |
+
"do_resize": true,
|
| 37 |
+
"image_mean": [
|
| 38 |
+
0.5,
|
| 39 |
+
0.5,
|
| 40 |
+
0.5
|
| 41 |
+
],
|
| 42 |
+
"image_processor_type": "Lfm3VlImageProcessor",
|
| 43 |
+
"image_std": [
|
| 44 |
+
0.5,
|
| 45 |
+
0.5,
|
| 46 |
+
0.5
|
| 47 |
+
],
|
| 48 |
+
"merge_size": 2,
|
| 49 |
+
"patch_size": 16,
|
| 50 |
+
"resample": 3,
|
| 51 |
+
"rescale_factor": 0.00392156862745098,
|
| 52 |
+
"size": {
|
| 53 |
+
"longest_edge": 2097152,
|
| 54 |
+
"shortest_edge": 65536
|
| 55 |
+
},
|
| 56 |
+
"temporal_patch_size": 2
|
| 57 |
+
},
|
| 58 |
+
"image_token": "<|image|>",
|
| 59 |
+
"processor_class": "Lfm3OmniProcessor",
|
| 60 |
+
"video_processor": {
|
| 61 |
+
"auto_map": {
|
| 62 |
+
"AutoProcessor": "processing_lfm3_omni.Lfm3OmniProcessor",
|
| 63 |
+
"AutoVideoProcessor": "video_processing_lfm3_vl.Lfm3VlVideoProcessor"
|
| 64 |
+
},
|
| 65 |
+
"cap_pixels_per_frame": false,
|
| 66 |
+
"do_convert_rgb": true,
|
| 67 |
+
"do_normalize": true,
|
| 68 |
+
"do_rescale": true,
|
| 69 |
+
"do_resize": true,
|
| 70 |
+
"do_sample_frames": true,
|
| 71 |
+
"fps": 2.0,
|
| 72 |
+
"image_mean": [
|
| 73 |
+
0.5,
|
| 74 |
+
0.5,
|
| 75 |
+
0.5
|
| 76 |
+
],
|
| 77 |
+
"image_std": [
|
| 78 |
+
0.5,
|
| 79 |
+
0.5,
|
| 80 |
+
0.5
|
| 81 |
+
],
|
| 82 |
+
"max_frames": 384,
|
| 83 |
+
"max_video_tokens": 768,
|
| 84 |
+
"merge_size": 2,
|
| 85 |
+
"min_frames": 4,
|
| 86 |
+
"patch_size": 16,
|
| 87 |
+
"resample": 3,
|
| 88 |
+
"rescale_factor": 0.00392156862745098,
|
| 89 |
+
"return_metadata": false,
|
| 90 |
+
"size": {
|
| 91 |
+
"longest_edge": 25165824,
|
| 92 |
+
"shortest_edge": 4096
|
| 93 |
+
},
|
| 94 |
+
"temporal_patch_size": 2,
|
| 95 |
+
"video_processor_type": "Lfm3VlVideoProcessor"
|
| 96 |
+
},
|
| 97 |
+
"video_token": "<|video|>",
|
| 98 |
+
"vision_end_token": "<|vision_end|>",
|
| 99 |
+
"vision_markers": true,
|
| 100 |
+
"vision_start_token": "<|vision_start|>"
|
| 101 |
+
}
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:41927bd314d17d7d7fadb44e10cd0919f7b71528d7741596d8cbd15c6945ec44
|
| 3 |
+
size 17908634
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"auto_map": {
|
| 3 |
+
"AutoProcessor": "processing_lfm3_omni.Lfm3OmniProcessor"
|
| 4 |
+
},
|
| 5 |
+
"backend": "tokenizers",
|
| 6 |
+
"bos_token": "<|startoftext|>",
|
| 7 |
+
"eos_token": "<|im_end|>",
|
| 8 |
+
"extra_special_tokens": [],
|
| 9 |
+
"image_token": "<|image|>",
|
| 10 |
+
"is_local": true,
|
| 11 |
+
"local_files_only": false,
|
| 12 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 13 |
+
"model_specific_special_tokens": {
|
| 14 |
+
"image_token": "<|image|>",
|
| 15 |
+
"video_token": "<|video|>",
|
| 16 |
+
"vision_end_token": "<|vision_end|>",
|
| 17 |
+
"vision_start_token": "<|vision_start|>"
|
| 18 |
+
},
|
| 19 |
+
"pad_token": "<|pad|>",
|
| 20 |
+
"processor_class": "Lfm3OmniProcessor",
|
| 21 |
+
"tokenizer_class": "TokenizersBackend",
|
| 22 |
+
"video_token": "<|video|>",
|
| 23 |
+
"vision_end_token": "<|vision_end|>",
|
| 24 |
+
"vision_start_token": "<|vision_start|>"
|
| 25 |
+
}
|
video_processing_lfm3_vl.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import math
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
from torchvision.transforms.v2 import functional as tvF
|
| 19 |
+
|
| 20 |
+
from transformers.image_processing_utils import BatchFeature
|
| 21 |
+
from transformers.image_utils import PILImageResampling, SizeDict
|
| 22 |
+
from transformers.processing_utils import Unpack, VideosKwargs
|
| 23 |
+
from transformers.utils import TensorType, auto_docstring, logging
|
| 24 |
+
from transformers.video_processing_utils import BaseVideoProcessor
|
| 25 |
+
from transformers.video_utils import (
|
| 26 |
+
VideoMetadata,
|
| 27 |
+
group_videos_by_shape,
|
| 28 |
+
make_batched_metadata,
|
| 29 |
+
make_batched_videos,
|
| 30 |
+
reorder_videos,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
logger = logging.get_logger(__name__)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Lfm3VlVideoProcessorInitKwargs(VideosKwargs, total=False):
|
| 38 |
+
r"""
|
| 39 |
+
patch_size (`int`, *optional*, defaults to 16):
|
| 40 |
+
The spatial patch size of the vision encoder.
|
| 41 |
+
temporal_patch_size (`int`, *optional*, defaults to 2):
|
| 42 |
+
The temporal patch size of the vision encoder.
|
| 43 |
+
merge_size (`int`, *optional*, defaults to 2):
|
| 44 |
+
The merge size of the vision encoder to llm encoder.
|
| 45 |
+
min_frames (`int`, *optional*, defaults to 4):
|
| 46 |
+
The minimum number of frames to sample from video.
|
| 47 |
+
max_frames (`int`, *optional*, defaults to 768):
|
| 48 |
+
The maximum number of frames to sample from video.
|
| 49 |
+
cap_pixels_per_frame (`bool`, *optional*):
|
| 50 |
+
Whether to cap each frame's pixel cost the way the reference implementation (qwen-vl-utils) does:
|
| 51 |
+
per-frame pixels are limited to `min(max_video_tokens * factor**2, size["longest_edge"] / num_frames)`
|
| 52 |
+
and floored at `1.05 * size["shortest_edge"]`, so token cost scales with clip duration. Without the
|
| 53 |
+
cap, videos that sample few frames spend the whole `size["longest_edge"]` budget on those frames and
|
| 54 |
+
keep near-native per-frame resolution, so a short clip can cost almost as many tokens as a long
|
| 55 |
+
video. If unset, the current uncapped behavior is kept and a warning is emitted: the default will
|
| 56 |
+
change to `True` in v5.22, after which the argument will be removed.
|
| 57 |
+
max_video_tokens (`int`, *optional*, defaults to 768):
|
| 58 |
+
The per-frame token ceiling applied by `cap_pixels_per_frame`, in vision tokens per frame
|
| 59 |
+
(qwen-vl-utils' `VIDEO_MAX_TOKEN_NUM`).
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
patch_size: int
|
| 63 |
+
temporal_patch_size: int
|
| 64 |
+
merge_size: int
|
| 65 |
+
min_frames: int
|
| 66 |
+
max_frames: int
|
| 67 |
+
cap_pixels_per_frame: bool
|
| 68 |
+
max_video_tokens: int
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def smart_resize(
|
| 72 |
+
num_frames: int,
|
| 73 |
+
height: int,
|
| 74 |
+
width: int,
|
| 75 |
+
temporal_factor: int = 2,
|
| 76 |
+
factor: int = 28,
|
| 77 |
+
min_pixels: int = 112 * 112,
|
| 78 |
+
max_pixels: int = 14 * 14 * 2 * 2 * 2 * 6144,
|
| 79 |
+
):
|
| 80 |
+
if num_frames < temporal_factor:
|
| 81 |
+
raise ValueError(f"t:{num_frames} must be larger than temporal_factor:{temporal_factor}")
|
| 82 |
+
if height < factor or width < factor:
|
| 83 |
+
scale = max(factor / height, factor / width)
|
| 84 |
+
height = int(height * scale)
|
| 85 |
+
width = int(width * scale)
|
| 86 |
+
|
| 87 |
+
if max(height, width) / min(height, width) > 200:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
f"absolute aspect ratio must be smaller than 200, got {max(height, width) / min(height, width)}"
|
| 90 |
+
)
|
| 91 |
+
h_bar = round(height / factor) * factor
|
| 92 |
+
w_bar = round(width / factor) * factor
|
| 93 |
+
t_bar = round(num_frames / temporal_factor) * temporal_factor
|
| 94 |
+
|
| 95 |
+
if t_bar * h_bar * w_bar > max_pixels:
|
| 96 |
+
beta = math.sqrt((num_frames * height * width) / max_pixels)
|
| 97 |
+
h_bar = max(factor, math.floor(height / beta / factor) * factor)
|
| 98 |
+
w_bar = max(factor, math.floor(width / beta / factor) * factor)
|
| 99 |
+
elif t_bar * h_bar * w_bar < min_pixels:
|
| 100 |
+
beta = math.sqrt(min_pixels / (num_frames * height * width))
|
| 101 |
+
h_bar = math.ceil(height * beta / factor) * factor
|
| 102 |
+
w_bar = math.ceil(width * beta / factor) * factor
|
| 103 |
+
|
| 104 |
+
return h_bar, w_bar
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _check_video_fps(metadata: VideoMetadata) -> None:
|
| 108 |
+
if metadata.fps is not None and not (math.isfinite(metadata.fps) and metadata.fps > 0):
|
| 109 |
+
raise ValueError(f"`video_metadata.fps` must be a positive number, got {metadata.fps}")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _check_video_metadata_count(videos, video_metadata) -> None:
|
| 113 |
+
# the shared sampler pairs clips with metadata by `zip`, which drops the clips past the last entry
|
| 114 |
+
if video_metadata is None:
|
| 115 |
+
return
|
| 116 |
+
videos = make_batched_videos(videos)
|
| 117 |
+
entries = make_batched_metadata(videos, video_metadata) if video_metadata != [] else []
|
| 118 |
+
if len(entries) != len(videos):
|
| 119 |
+
raise ValueError(
|
| 120 |
+
f"Received {len(videos)} videos but {len(entries)} `video_metadata` entries; pass one entry per video"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@auto_docstring
|
| 125 |
+
class Lfm3VlVideoProcessor(BaseVideoProcessor):
|
| 126 |
+
resample = PILImageResampling.BICUBIC
|
| 127 |
+
size = {"shortest_edge": 4_096, "longest_edge": 25_165_824}
|
| 128 |
+
image_mean = [0.5, 0.5, 0.5]
|
| 129 |
+
image_std = [0.5, 0.5, 0.5]
|
| 130 |
+
do_resize = True
|
| 131 |
+
do_rescale = True
|
| 132 |
+
do_normalize = True
|
| 133 |
+
do_convert_rgb = True
|
| 134 |
+
patch_size = 16
|
| 135 |
+
temporal_patch_size = 2
|
| 136 |
+
merge_size = 2
|
| 137 |
+
min_frames = 4
|
| 138 |
+
max_frames = 768
|
| 139 |
+
do_sample_frames = True
|
| 140 |
+
cap_pixels_per_frame = False
|
| 141 |
+
max_video_tokens = 768
|
| 142 |
+
valid_kwargs = Lfm3VlVideoProcessorInitKwargs
|
| 143 |
+
model_input_names = ["pixel_values_videos", "video_grid_thw"]
|
| 144 |
+
max_image_size = {"longest_edge": 28 * 28 * 2 * 30000}
|
| 145 |
+
max_duration = None
|
| 146 |
+
num_frames = None
|
| 147 |
+
fps = 2.0
|
| 148 |
+
|
| 149 |
+
def __init__(self, **kwargs: Unpack[Lfm3VlVideoProcessorInitKwargs]):
|
| 150 |
+
super().__init__(**kwargs)
|
| 151 |
+
|
| 152 |
+
def sample_frames(
|
| 153 |
+
self, metadata: VideoMetadata, num_frames: int | None = None, fps: int | float | None = None, **kwargs
|
| 154 |
+
):
|
| 155 |
+
"""Decoded frames whose metadata carries no fps are sampled and timestamped at the 24 fps the shared sampler
|
| 156 |
+
assumes (with a warning); pass `video_metadata` with the clip's fps for accurate timestamps."""
|
| 157 |
+
_check_video_fps(metadata)
|
| 158 |
+
if fps is not None and not (math.isfinite(fps) and fps > 0):
|
| 159 |
+
raise ValueError(f"`fps` must be a positive number, got {fps}")
|
| 160 |
+
if fps is not None and num_frames is not None:
|
| 161 |
+
raise ValueError("`num_frames` and `fps` are mutually exclusive arguments, please use only one!")
|
| 162 |
+
|
| 163 |
+
total_num_frames = metadata.total_num_frames
|
| 164 |
+
fps = fps if fps is not None else self.fps
|
| 165 |
+
|
| 166 |
+
# If num_frames is not given but fps is, calculate num_frames from fps
|
| 167 |
+
if num_frames is None and fps is not None:
|
| 168 |
+
if metadata.fps is None:
|
| 169 |
+
metadata.fps = 24
|
| 170 |
+
logger.warning_once(
|
| 171 |
+
"Asked to sample `fps` frames per second but no video metadata was provided which is required when sampling with `fps`. "
|
| 172 |
+
"Defaulting to `fps=24`. Please provide `video_metadata` for more accurate results."
|
| 173 |
+
)
|
| 174 |
+
num_frames = int(total_num_frames / metadata.fps * fps)
|
| 175 |
+
num_frames = min(max(num_frames, self.min_frames), self.max_frames, total_num_frames)
|
| 176 |
+
|
| 177 |
+
if num_frames is None:
|
| 178 |
+
num_frames = min(max(total_num_frames, self.min_frames), self.max_frames)
|
| 179 |
+
|
| 180 |
+
indices = np.linspace(0, total_num_frames - 1, num_frames).round().astype(int)
|
| 181 |
+
|
| 182 |
+
return indices
|
| 183 |
+
|
| 184 |
+
def resize(
|
| 185 |
+
self,
|
| 186 |
+
videos: "torch.Tensor",
|
| 187 |
+
size: SizeDict,
|
| 188 |
+
resample: "PILImageResampling | tvF.InterpolationMode | int | None",
|
| 189 |
+
factor: int,
|
| 190 |
+
temporal_factor: int,
|
| 191 |
+
cap_pixels_per_frame: bool | None = None,
|
| 192 |
+
**kwargs,
|
| 193 |
+
) -> "torch.Tensor":
|
| 194 |
+
"""Resize dynamically based on input video aspect ratio."""
|
| 195 |
+
if not size.shortest_edge or not size.longest_edge:
|
| 196 |
+
raise ValueError(f"`size` dict must contain 'shortest_edge' and 'longest_edge' keys but got {size}.")
|
| 197 |
+
|
| 198 |
+
num_frames = videos.shape[1]
|
| 199 |
+
max_pixels = size.longest_edge
|
| 200 |
+
if cap_pixels_per_frame:
|
| 201 |
+
# per-frame pixels are capped at `max_video_tokens` patches or the budget's even share per frame
|
| 202 |
+
frame_cap = self.max_video_tokens * factor * factor
|
| 203 |
+
pixels_per_frame = max(min(frame_cap, size.longest_edge // num_frames), int(size.shortest_edge * 1.05))
|
| 204 |
+
max_pixels = pixels_per_frame * num_frames
|
| 205 |
+
|
| 206 |
+
height, width = videos.shape[-2:]
|
| 207 |
+
resized_height, resized_width = smart_resize(
|
| 208 |
+
height=height,
|
| 209 |
+
width=width,
|
| 210 |
+
num_frames=num_frames,
|
| 211 |
+
factor=factor,
|
| 212 |
+
temporal_factor=temporal_factor,
|
| 213 |
+
min_pixels=size.shortest_edge,
|
| 214 |
+
max_pixels=max_pixels,
|
| 215 |
+
)
|
| 216 |
+
return super().resize(
|
| 217 |
+
image=videos,
|
| 218 |
+
size=SizeDict(height=resized_height, width=resized_width),
|
| 219 |
+
resample=resample,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
def patchify(
|
| 223 |
+
self,
|
| 224 |
+
videos: "torch.Tensor",
|
| 225 |
+
patch_size: int,
|
| 226 |
+
merge_size: int,
|
| 227 |
+
temporal_patch_size: int,
|
| 228 |
+
) -> tuple["torch.Tensor", int, int]:
|
| 229 |
+
"Patchifies each video into flat layout of shape (`seq_len`, `patch_dim`) so we can concat dynamically shaped pixels."
|
| 230 |
+
batch_size, num_frames, channel, resized_height, resized_width = videos.shape
|
| 231 |
+
|
| 232 |
+
# Check that videos have `num_frames` divisible by `temporal_patch_size`
|
| 233 |
+
if pad := -num_frames % temporal_patch_size:
|
| 234 |
+
repeats = videos[:, -1:].expand(-1, pad, -1, -1, -1)
|
| 235 |
+
videos = torch.cat((videos, repeats), dim=1)
|
| 236 |
+
num_frames += pad
|
| 237 |
+
|
| 238 |
+
grid_t = num_frames // temporal_patch_size
|
| 239 |
+
grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
|
| 240 |
+
|
| 241 |
+
patches = videos.view(
|
| 242 |
+
batch_size,
|
| 243 |
+
grid_t,
|
| 244 |
+
temporal_patch_size,
|
| 245 |
+
channel,
|
| 246 |
+
grid_h // merge_size,
|
| 247 |
+
merge_size,
|
| 248 |
+
patch_size,
|
| 249 |
+
grid_w // merge_size,
|
| 250 |
+
merge_size,
|
| 251 |
+
patch_size,
|
| 252 |
+
)
|
| 253 |
+
patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
|
| 254 |
+
flatten_patches = patches.reshape(
|
| 255 |
+
batch_size,
|
| 256 |
+
grid_t * grid_h * grid_w,
|
| 257 |
+
channel * temporal_patch_size * patch_size * patch_size,
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
return flatten_patches, grid_t, grid_h, grid_w
|
| 261 |
+
|
| 262 |
+
def _preprocess(
|
| 263 |
+
self,
|
| 264 |
+
videos: list["torch.Tensor"],
|
| 265 |
+
do_resize: bool,
|
| 266 |
+
size: SizeDict,
|
| 267 |
+
resample: "PILImageResampling | tvF.InterpolationMode | int | None",
|
| 268 |
+
do_rescale: bool,
|
| 269 |
+
rescale_factor: float,
|
| 270 |
+
do_normalize: bool,
|
| 271 |
+
image_mean: float | list[float] | None,
|
| 272 |
+
image_std: float | list[float] | None,
|
| 273 |
+
patch_size: int | None = None,
|
| 274 |
+
temporal_patch_size: int | None = None,
|
| 275 |
+
merge_size: int | None = None,
|
| 276 |
+
cap_pixels_per_frame: bool | None = None,
|
| 277 |
+
return_tensors: str | TensorType | None = None,
|
| 278 |
+
**kwargs,
|
| 279 |
+
):
|
| 280 |
+
if cap_pixels_per_frame is None:
|
| 281 |
+
logger.warning_once(
|
| 282 |
+
"Lfm3Vl video processing does not apply the per-frame pixel cap the reference "
|
| 283 |
+
"implementation (qwen-vl-utils) applies, so some videos cost far more tokens than they "
|
| 284 |
+
"would there. In v5.22 the capped behavior will become the default and "
|
| 285 |
+
"`cap_pixels_per_frame` will be removed. Pass `cap_pixels_per_frame=True` to adopt the "
|
| 286 |
+
"reference behavior now, or `False` to keep the current behavior and silence this "
|
| 287 |
+
"warning."
|
| 288 |
+
)
|
| 289 |
+
cap_pixels_per_frame = False
|
| 290 |
+
# Group videos by size for batched resizing
|
| 291 |
+
grouped_videos, grouped_videos_index = group_videos_by_shape(videos)
|
| 292 |
+
resized_videos_grouped = {}
|
| 293 |
+
for shape, stacked_videos in grouped_videos.items():
|
| 294 |
+
if do_resize:
|
| 295 |
+
stacked_videos = self.resize(
|
| 296 |
+
videos=stacked_videos,
|
| 297 |
+
size=size,
|
| 298 |
+
resample=resample,
|
| 299 |
+
factor=patch_size * merge_size,
|
| 300 |
+
temporal_factor=temporal_patch_size,
|
| 301 |
+
cap_pixels_per_frame=cap_pixels_per_frame,
|
| 302 |
+
)
|
| 303 |
+
resized_videos_grouped[shape] = stacked_videos
|
| 304 |
+
resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index)
|
| 305 |
+
|
| 306 |
+
# Group videos by size for further processing
|
| 307 |
+
# Needed in case do_resize is False, or resize returns videos with different sizes
|
| 308 |
+
grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos)
|
| 309 |
+
processed_videos_grouped = {}
|
| 310 |
+
processed_grids = {}
|
| 311 |
+
for shape, stacked_videos in grouped_videos.items():
|
| 312 |
+
# Fused rescale and normalize
|
| 313 |
+
stacked_videos = self.rescale_and_normalize(
|
| 314 |
+
stacked_videos, do_rescale, rescale_factor, do_normalize, image_mean, image_std
|
| 315 |
+
)
|
| 316 |
+
patches, grid_t, grid_h, grid_w = self.patchify(
|
| 317 |
+
stacked_videos,
|
| 318 |
+
patch_size=patch_size,
|
| 319 |
+
merge_size=merge_size,
|
| 320 |
+
temporal_patch_size=temporal_patch_size,
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
processed_videos_grouped[shape] = patches
|
| 324 |
+
processed_grids[shape] = [[grid_t, grid_h, grid_w]] * len(stacked_videos)
|
| 325 |
+
|
| 326 |
+
processed_videos = reorder_videos(processed_videos_grouped, grouped_videos_index)
|
| 327 |
+
processed_grids = reorder_videos(processed_grids, grouped_videos_index)
|
| 328 |
+
pixel_values_videos = torch.cat(processed_videos, dim=0)
|
| 329 |
+
video_grid_thw = torch.tensor(processed_grids)
|
| 330 |
+
|
| 331 |
+
return BatchFeature(
|
| 332 |
+
data={"pixel_values_videos": pixel_values_videos, "video_grid_thw": video_grid_thw},
|
| 333 |
+
tensor_type=return_tensors,
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def get_num_of_video_patches(self, num_frames: int, height: int, width: int, videos_kwargs=None):
|
| 337 |
+
"""
|
| 338 |
+
A utility that returns number of video patches a given video size.
|
| 339 |
+
|
| 340 |
+
Args:
|
| 341 |
+
num_frames (`int`):
|
| 342 |
+
Number of frames in the input video.
|
| 343 |
+
height (`int`):
|
| 344 |
+
Height of the input video.
|
| 345 |
+
width (`int`):
|
| 346 |
+
Width of the input video.
|
| 347 |
+
videos_kwargs (`dict`, *optional*)
|
| 348 |
+
Any kwargs to override defaults of the video processor.
|
| 349 |
+
Returns:
|
| 350 |
+
`int`: Number of video patches per video.
|
| 351 |
+
"""
|
| 352 |
+
videos_kwargs = videos_kwargs if videos_kwargs is not None else {}
|
| 353 |
+
size = videos_kwargs.get("size", None) or self.size
|
| 354 |
+
min_pixels = size["shortest_edge"]
|
| 355 |
+
max_pixels = size["longest_edge"]
|
| 356 |
+
patch_size = videos_kwargs.get("patch_size", None) or self.patch_size
|
| 357 |
+
merge_size = videos_kwargs.get("merge_size", None) or self.merge_size
|
| 358 |
+
temporal_patch_size = videos_kwargs.get("temporal_patch_size", None) or self.temporal_patch_size
|
| 359 |
+
cap_pixels_per_frame = videos_kwargs.get("cap_pixels_per_frame", None) or self.cap_pixels_per_frame
|
| 360 |
+
|
| 361 |
+
factor = patch_size * merge_size
|
| 362 |
+
if cap_pixels_per_frame:
|
| 363 |
+
# Keep the count in sync with `resize` when the per-frame cap is active.
|
| 364 |
+
frame_cap = self.max_video_tokens * factor * factor
|
| 365 |
+
pixels_per_frame = max(min(frame_cap, max_pixels // num_frames), int(min_pixels * 1.05))
|
| 366 |
+
max_pixels = pixels_per_frame * num_frames
|
| 367 |
+
|
| 368 |
+
resized_height, resized_width = smart_resize(
|
| 369 |
+
num_frames,
|
| 370 |
+
height,
|
| 371 |
+
width,
|
| 372 |
+
temporal_factor=temporal_patch_size,
|
| 373 |
+
factor=factor,
|
| 374 |
+
min_pixels=min_pixels,
|
| 375 |
+
max_pixels=max_pixels,
|
| 376 |
+
)
|
| 377 |
+
grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
|
| 378 |
+
grid_t = (num_frames + -num_frames % temporal_patch_size) // temporal_patch_size
|
| 379 |
+
return grid_t * grid_h * grid_w
|
| 380 |
+
|
| 381 |
+
def preprocess(self, videos, **kwargs):
|
| 382 |
+
_check_video_metadata_count(videos, kwargs.get("video_metadata"))
|
| 383 |
+
# the shared `preprocess` fills every unset option from the instance before `sample_frames` runs, so the
|
| 384 |
+
# caller's options are told apart from the instance ones here: a caller option beats an instance one, and a
|
| 385 |
+
# configured `num_frames` beats the default fps
|
| 386 |
+
num_frames, fps = kwargs.get("num_frames"), kwargs.get("fps")
|
| 387 |
+
if num_frames is not None and fps is not None:
|
| 388 |
+
raise ValueError("`num_frames` and `fps` are mutually exclusive arguments, please use only one!")
|
| 389 |
+
if num_frames is not None or (fps is None and self.num_frames is not None):
|
| 390 |
+
kwargs["fps"] = None
|
| 391 |
+
elif fps is not None:
|
| 392 |
+
kwargs["num_frames"] = None
|
| 393 |
+
return super().preprocess(videos, **kwargs)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
__all__ = ["Lfm3VlVideoProcessor"]
|