Fix Mammoth’s CLI selecting the wrong engine

#3
by sheppo - opened
Files changed (2) hide show
  1. serve.patch +19 -0
  2. test_cli_routing.py +68 -0
serve.patch CHANGED
@@ -783,3 +783,22 @@ index 4f6f7bd..ae428d9 100644
783
  try:
784
  opt.step()
785
  except torch.OutOfMemoryError:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783
  try:
784
  opt.step()
785
  except torch.OutOfMemoryError:
786
+ diff --git a/qev/cli.py b/qev/cli.py
787
+ --- a/qev/cli.py
788
+ +++ b/qev/cli.py
789
+ @@ -115,7 +115,14 @@
790
+ if calib_path:
791
+ logging.getLogger("qev.cli").info("calibration: %s", calib_path)
792
+ config = json.loads((Path(model_path) / "config.json").read_text())
793
+ - if args.multimodal or config.get("vision_config") is not None:
794
+ + from .readout import ReadoutConfig
795
+ +
796
+ + # Only the patched text engine implements block-bidirectional attention.
797
+ + noncausal = ReadoutConfig.load(model_path).noncausal
798
+ + if args.multimodal and noncausal:
799
+ + raise SystemExit("Block-bidirectional attention is unsupported by the multimodal engine; "
800
+ + "omit --multimodal to serve text requests with DecisionEngine.")
801
+ + if args.multimodal or (config.get("vision_config") is not None and not noncausal):
802
+ from .mm_engine import MMDecisionEngine
803
+
804
+ engine = MMDecisionEngine(model_path, device=args.device, dtype=args.dtype, calibration=calib,
test_cli_routing.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only routing regression: python test_cli_routing.py /path/to/patched/OneJev."""
2
+ import argparse
3
+ import importlib.util
4
+ import contextlib
5
+ import json
6
+ from pathlib import Path
7
+ import sys
8
+ import tempfile
9
+ import types
10
+ import unittest
11
+ from unittest.mock import patch
12
+
13
+ ROOT = Path(sys.argv.pop(1))
14
+
15
+ class RoutingTest(unittest.TestCase):
16
+ def route(self, config, readout=None, multimodal=False):
17
+ # Import the actual readout module and execute the CLI; stub GPU/server boundaries.
18
+ called = []
19
+ def engine(kind):
20
+ return lambda *a, **kw: called.append(kind) or object()
21
+ mods = {}
22
+ for name, attrs in {
23
+ 'uvicorn': {'run': lambda *a, **kw: None},
24
+ 'qev': {}, 'qev.calibrate': {'Calibration': object},
25
+ 'qev.engine': {'DecisionEngine': engine('text')},
26
+ 'qev.mm_engine': {'MMDecisionEngine': engine('multimodal'), 'local_model_dir': lambda x: x},
27
+ 'torch': {'nn': types.SimpleNamespace(Module=object)},
28
+ 'qev.prompt': {'MAX_SLOTS': 256},
29
+ 'qev.server': {'create_app': lambda *a, **kw: object()},
30
+ }.items():
31
+ mods[name] = types.ModuleType(name)
32
+ mods[name].__dict__.update(attrs)
33
+ with tempfile.TemporaryDirectory() as d, patch.dict(sys.modules, mods), patch('importlib.util.find_spec', return_value=object()):
34
+ Path(d, 'config.json').write_text(json.dumps(config))
35
+ if readout is not None:
36
+ Path(d, 'readout.json').write_text(json.dumps(readout))
37
+ spec = importlib.util.spec_from_file_location('qev.readout', ROOT / 'qev/readout.py')
38
+ module = importlib.util.module_from_spec(spec)
39
+ sys.modules['qev.readout'] = module
40
+ spec.loader.exec_module(module)
41
+ cli = {'__name__': 'qev.cli', '__package__': 'qev'}
42
+ exec(compile((ROOT / 'qev/cli.py').read_text(), 'cli.py', 'exec'), cli)
43
+ args = argparse.Namespace(model=d, calibration=None, multimodal=multimodal,
44
+ gguf=None, llama_url=None, device='cpu', dtype='bfloat16', fork_mode='sequential',
45
+ head_dtype='float32', media_root=None, no_gpu_preprocess=False, no_cuda_graphs=False,
46
+ quantize=None, max_branch_tokens=16384, max_request_tokens=65536,
47
+ name=None, host='127.0.0.1', port=8000)
48
+ cli['cmd_serve'](args)
49
+ return called
50
+
51
+ def test_routes(self):
52
+ layout = {'vision_config': {}, 'modeling_overrides': {'attention': {'full_attention': 'block_bidirectional'}}}
53
+ for config, readout, explicit, expected in [
54
+ (layout, None, False, 'text'), ({}, None, False, 'text'),
55
+ ({'vision_config': {}}, None, False, 'multimodal'),
56
+ ({'vision_config': {}}, None, True, 'multimodal'),
57
+ ({'vision_config': {}}, {'attention': 'noncausal'}, False, 'text'),
58
+ ({'vision_config': {}}, {'readout': 'head'}, False, 'multimodal'),
59
+ ]:
60
+ with self.subTest(config=config, readout=readout, explicit=explicit):
61
+ self.assertEqual(self.route(config, readout, explicit), [expected])
62
+ with self.assertRaisesRegex(SystemExit, 'omit --multimodal'):
63
+ self.route(layout, multimodal=True)
64
+ with self.assertRaisesRegex(ValueError, 'unsupported full_attention'):
65
+ self.route({'modeling_overrides': {'attention': {'full_attention': 'unknown'}}})
66
+
67
+ if __name__ == '__main__':
68
+ unittest.main()