Skip to content

Commit 68a408a

Browse files
feat(lfm2_5_vl): add BUCK file, activation stats logging, and smoke tests
- BUCK: python_library + export_lib + binary targets following llava/BUCK and lfm2/BUCK patterns - export_all: log non_const_buffer_sizes per execution plan (matches llava) - export_all: add _return_program=True for tests (avoids writing to disk) - test/test_lfm2_5_vl.py: vision encoder shape, prefill shape, export method names, and end-to-end prefill+decode loop tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 40559c0 commit 68a408a

3 files changed

Lines changed: 183 additions & 3 deletions

File tree

examples/models/lfm2_5_vl/BUCK

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target", "non_fbcode_target")
2+
# Any targets that should be shared between fbcode and xplat must be defined in
3+
# targets.bzl. This file can contain fbcode-only targets.
4+
5+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
6+
7+
oncall("executorch")
8+
9+
fbcode_target(_kind = runtime.python_library,
10+
name = "lfm2_5_vl",
11+
srcs = [
12+
"__init__.py",
13+
"convert_weights.py",
14+
"model.py",
15+
],
16+
resources = {
17+
"config/lfm2_5_vl_1_6b_config.json": "config/lfm2_5_vl_1_6b_config.json",
18+
},
19+
base_module = "executorch.examples.models.lfm2_5_vl",
20+
visibility = ["PUBLIC"],
21+
deps = [
22+
"//caffe2:torch",
23+
"//executorch/examples/models/llama:transformer_modules",
24+
"//executorch/examples/models/llama:export_library",
25+
"fbsource//third-party/pypi/safetensors:safetensors",
26+
"fbsource//third-party/pypi/transformers:transformers",
27+
],
28+
)
29+
30+
fbcode_target(_kind = runtime.python_library,
31+
name = "export_lib",
32+
srcs = [
33+
"export_lfm2_5_vl.py",
34+
],
35+
_is_external_target = True,
36+
base_module = "executorch.examples.models.lfm2_5_vl",
37+
visibility = [
38+
"//executorch/...",
39+
],
40+
deps = [
41+
":lfm2_5_vl",
42+
],
43+
)
44+
45+
fbcode_target(_kind = runtime.python_binary,
46+
name = "export",
47+
main_function = "executorch.examples.models.lfm2_5_vl.export_lfm2_5_vl.main",
48+
preload_deps = [
49+
"//executorch/extension/llm/custom_ops:custom_ops_aot_lib",
50+
"//executorch/kernels/quantized:aot_lib",
51+
],
52+
deps = [
53+
":export_lib",
54+
],
55+
)

examples/models/lfm2_5_vl/export_lfm2_5_vl.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,14 @@ def export_token_embedding(
232232

233233
def export_all(
234234
model_dir: str,
235-
output: str,
235+
output: Optional[str],
236236
dtype: DType = DType.fp32,
237237
quantize: bool = False,
238238
max_seq_len: int = MAX_SEQ_LEN,
239239
max_context_len: int = MAX_SEQ_LEN,
240240
params_path: Optional[str] = None,
241-
) -> None:
241+
_return_program: bool = False,
242+
):
242243
logging.info(f"Loading {model_dir}...")
243244
lfm2_model = Lfm2p5VlModel(
244245
model_dir=model_dir,
@@ -309,8 +310,16 @@ def export_all(
309310
)
310311
)
311312

313+
for execution_plan in et_program._emitter_output.program.execution_plan:
314+
logging.info(
315+
f"Required memory for activation in bytes: {execution_plan.non_const_buffer_sizes}"
316+
)
317+
318+
if _return_program:
319+
return et_program
320+
312321
logging.info(f"Saving {output}...")
313-
with open(output, "wb") as f:
322+
with open(output, "wb") as f: # type: ignore[arg-type]
314323
et_program.write_to_file(f)
315324
logging.info(f"Saved {output}. Methods: {et_program.methods}")
316325

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import logging
8+
import unittest
9+
10+
import torch
11+
from executorch.examples.models.lfm2_5_vl.export_lfm2_5_vl import export_all
12+
from executorch.examples.models.lfm2_5_vl.model import IMAGE_SIZE, MAX_SEQ_LEN, Lfm2p5VlModel
13+
14+
# import order matters: portable_lib must come first so its static op registry
15+
# is in place before custom_ops registers against it.
16+
from executorch.extension.pybindings.portable_lib import ( # noqa # usort: skip
17+
_load_for_executorch_from_buffer,
18+
)
19+
from executorch.extension.llm.custom_ops import custom_ops # noqa # usort: skip
20+
from executorch.kernels import quantized # noqa # usort: skip
21+
22+
logging.basicConfig(level=logging.INFO)
23+
logger = logging.getLogger(__name__)
24+
25+
MODEL_DIR = "LiquidAI/LFM2-VL-1.6B"
26+
27+
28+
class TestLfm2p5Vl(unittest.TestCase):
29+
@classmethod
30+
def setUpClass(cls):
31+
cls.lfm2_model = Lfm2p5VlModel(model_dir=MODEL_DIR)
32+
cls.lfm2 = cls.lfm2_model.get_eager_model().eval()
33+
34+
def test_vision_encoder_shape(self):
35+
"""Vision encoder must produce [1, 256, 2048] embeddings."""
36+
pixels = torch.randint(0, 256, (1, 3, IMAGE_SIZE, IMAGE_SIZE), dtype=torch.float32)
37+
with torch.no_grad():
38+
embeds = self.lfm2.image_embedding(pixels)
39+
self.assertEqual(embeds.shape, (1, 256, 2048))
40+
41+
def test_prefill_output_shape(self):
42+
"""Prefill must return (seq_len: int, logits [1, vocab_size])."""
43+
prompt_before, pixels, prompt_after = self.lfm2_model.get_inputs_for_prefill()
44+
with torch.no_grad():
45+
seq_len, logits = self.lfm2.prefill(prompt_before, pixels, prompt_after)
46+
self.assertIsInstance(seq_len, int)
47+
self.assertEqual(logits.shape[-1], 65536)
48+
49+
def test_export_methods(self):
50+
"""Exported PTE must contain the three named methods and metadata."""
51+
et_program = export_all(
52+
model_dir=MODEL_DIR,
53+
output=None, # in-memory only
54+
_return_program=True,
55+
)
56+
self.assertIn("vision_encoder", et_program.methods)
57+
self.assertIn("token_embedding", et_program.methods)
58+
self.assertIn("text_decoder", et_program.methods)
59+
60+
def test_export_and_run(self):
61+
"""Export to PTE and run a short prefill + decode loop end-to-end."""
62+
et_program = export_all(
63+
model_dir=MODEL_DIR,
64+
output=None,
65+
_return_program=True,
66+
)
67+
module = _load_for_executorch_from_buffer(et_program.buffer)
68+
69+
prompt_before, pixels, prompt_after = self.lfm2_model.get_inputs_for_prefill()
70+
start_pos = 0
71+
72+
# Embed and prefill tokens before image
73+
before_embeds = module.run_method("token_embedding", (prompt_before,))[0]
74+
module.run_method(
75+
"text_decoder",
76+
(before_embeds, torch.arange(start_pos, start_pos + before_embeds.shape[1])),
77+
)
78+
start_pos += before_embeds.shape[1]
79+
80+
# Vision encoder
81+
image_embeds = module.run_method("vision_encoder", (pixels,))[0]
82+
module.run_method(
83+
"text_decoder",
84+
(image_embeds, torch.arange(start_pos, start_pos + image_embeds.shape[1])),
85+
)
86+
start_pos += image_embeds.shape[1]
87+
88+
# Embed and prefill tokens after image
89+
after_embeds = module.run_method("token_embedding", (prompt_after,))[0]
90+
logits = module.run_method(
91+
"text_decoder",
92+
(after_embeds, torch.arange(start_pos, start_pos + after_embeds.shape[1])),
93+
)[0]
94+
start_pos += after_embeds.shape[1]
95+
96+
# Decode a few tokens — just check we get valid token IDs
97+
new_tokens = [torch.argmax(logits).item()]
98+
for i in range(3):
99+
token_embed = module.run_method(
100+
"token_embedding",
101+
(torch.tensor([[new_tokens[i]]], dtype=torch.int64),),
102+
)[0]
103+
logits = module.run_method(
104+
"text_decoder",
105+
(token_embed, torch.tensor([start_pos + i], dtype=torch.int64)),
106+
)[0]
107+
new_tokens.append(torch.argmax(logits).item())
108+
109+
self.assertEqual(len(new_tokens), 4)
110+
for tok in new_tokens:
111+
self.assertGreaterEqual(tok, 0)
112+
self.assertLess(tok, 65536)
113+
114+
115+
if __name__ == "__main__":
116+
unittest.main()

0 commit comments

Comments
 (0)