quapp-merlin 0.0.1.dev2__tar.gz → 0.0.1.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {quapp_merlin-0.0.1.dev2/quapp_merlin.egg-info → quapp_merlin-0.0.1.dev3}/PKG-INFO +1 -1
  2. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/pyproject.toml +1 -1
  3. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/merlin_remote_device.py +9 -2
  4. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3/quapp_merlin.egg-info}/PKG-INFO +1 -1
  5. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/SOURCES.txt +1 -0
  6. quapp_merlin-0.0.1.dev3/tests/test_remote_eval_mode.py +106 -0
  7. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/LICENSE +0 -0
  8. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/README.md +0 -0
  9. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/__init__.py +0 -0
  10. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/component/__init__.py +0 -0
  11. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/component/backend/__init__.py +0 -0
  12. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/component/backend/merlin_invocation.py +0 -0
  13. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/data/__init__.py +0 -0
  14. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/data/merlin_job_spec.py +0 -0
  15. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/error_rules.py +0 -0
  16. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/__init__.py +0 -0
  17. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/merlin_device_factory.py +0 -0
  18. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/merlin_handler_factory.py +0 -0
  19. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/factory/merlin_provider_factory.py +0 -0
  20. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/handler/__init__.py +0 -0
  21. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/handler/invocation_handler.py +0 -0
  22. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/__init__.py +0 -0
  23. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/__init__.py +0 -0
  24. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/merlin_device.py +0 -0
  25. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/device/merlin_local_device.py +0 -0
  26. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/provider/__init__.py +0 -0
  27. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/provider/quandela_merlin_provider.py +0 -0
  28. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/model/provider/quapp_merlin_provider.py +0 -0
  29. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/util/__init__.py +0 -0
  30. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/util/single_machine.py +0 -0
  31. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin/util/tensors.py +0 -0
  32. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/dependency_links.txt +0 -0
  33. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/requires.txt +0 -0
  34. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/quapp_merlin.egg-info/top_level.txt +0 -0
  35. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/setup.cfg +0 -0
  36. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_error_logging.py +0 -0
  37. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_error_rules.py +0 -0
  38. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_job_spec.py +0 -0
  39. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_lazy_sdk_imports.py +0 -0
  40. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_measurement_kind.py +0 -0
  41. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_single_machine.py +0 -0
  42. {quapp_merlin-0.0.1.dev2 → quapp_merlin-0.0.1.dev3}/tests/test_tensors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-merlin
3
- Version: 0.0.1.dev2
3
+ Version: 0.0.1.dev3
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quapp-merlin"
7
- version = "0.0.1.dev2"
7
+ version = "0.0.1.dev3"
8
8
  description = "Quapp common library supporting Quapp Platform for Quantum Computing"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
@@ -92,9 +92,16 @@ class MerlinRemoteDevice(MerlinDevice):
92
92
  f'Submitting to {self.device_specification} | batch={batch_size} '
93
93
  f'| shots={shots} | timeout={TIMEOUT_SECONDS}s')
94
94
 
95
+ # forward_async refuses a module still in training mode -- "no gradient
96
+ # can flow through the QPU runs" (merlin_processor.py:1109, it reads
97
+ # module.training). A torch Module is training=True from the moment it
98
+ # is built, so a handler that never calls .eval() is rejected every
99
+ # single time. The local device already switched to eval before running
100
+ # inference; this one did not.
101
+ model = spec.model.eval()
102
+
95
103
  started = time.perf_counter()
96
- future = processor.forward_async(spec.model, spec.inputs,
97
- nsample=shots)
104
+ future = processor.forward_async(model, spec.inputs, nsample=shots)
98
105
 
99
106
  job_ids = list(getattr(future, 'job_ids', None) or [])
100
107
  self.logger.info(f'Remote job submitted: job_ids={job_ids}')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quapp-merlin
3
- Version: 0.0.1.dev2
3
+ Version: 0.0.1.dev3
4
4
  Summary: Quapp common library supporting Quapp Platform for Quantum Computing
5
5
  Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
6
6
  License: The MIT License (MIT)
@@ -35,5 +35,6 @@ tests/test_error_rules.py
35
35
  tests/test_job_spec.py
36
36
  tests/test_lazy_sdk_imports.py
37
37
  tests/test_measurement_kind.py
38
+ tests/test_remote_eval_mode.py
38
39
  tests/test_single_machine.py
39
40
  tests/test_tensors.py
@@ -0,0 +1,106 @@
1
+ # Quapp Platform Project
2
+ # test_remote_eval_mode.py
3
+ # Copyright © CITYNOW Co. Ltd. All rights reserved.
4
+
5
+ """A model sent to the cloud has to be in eval mode.
6
+
7
+ MerlinProcessor refuses anything else::
8
+
9
+ if module.training:
10
+ raise RuntimeError(
11
+ "Backend quantum execution requires `.eval()` mode because no
12
+ gradient can flow through the QPU runs...")
13
+
14
+ -- merlinquantum 0.4.1, merlin/core/merlin_processor.py:1109.
15
+
16
+ A torch Module is ``training=True`` from the moment it is constructed, so a
17
+ handler that never calls ``.eval()`` -- which is every handler, because nothing
18
+ in the contract tells them to -- was rejected on every single remote job. The
19
+ local device already switched to eval before inference; the remote one did not.
20
+
21
+ Checked by reading the source rather than by running a model: importing merlin
22
+ would drag torch into the test suite, and the rule being enforced is "the call
23
+ is there, before the submit", which the AST answers exactly.
24
+ """
25
+
26
+ import ast
27
+ import pathlib
28
+
29
+ import pytest
30
+
31
+ REMOTE_DEVICE = (pathlib.Path(__file__).resolve().parent.parent
32
+ / 'quapp_merlin' / 'model' / 'device'
33
+ / 'merlin_remote_device.py')
34
+ LOCAL_DEVICE = (pathlib.Path(__file__).resolve().parent.parent
35
+ / 'quapp_merlin' / 'model' / 'device'
36
+ / 'merlin_local_device.py')
37
+
38
+
39
+ def create_job(path):
40
+ tree = ast.parse(path.read_text(encoding='utf-8'), filename=str(path))
41
+ for node in ast.walk(tree):
42
+ if isinstance(node, ast.FunctionDef) and node.name == '_create_job':
43
+ return node
44
+ raise AssertionError(f'_create_job() not found in {path.name}')
45
+
46
+
47
+ def call_lines(node, attribute) -> list:
48
+ """Line numbers of every ``x.<attribute>(...)`` call inside a function."""
49
+ return sorted(inner.lineno for inner in ast.walk(node)
50
+ if isinstance(inner, ast.Call)
51
+ and getattr(inner.func, 'attr', None) == attribute)
52
+
53
+
54
+ def test_remote_switches_the_model_to_eval():
55
+ assert call_lines(create_job(REMOTE_DEVICE), 'eval'), (
56
+ 'merlin_remote_device._create_job never calls .eval(); MerlinProcessor '
57
+ 'rejects a module in training mode and every remote job fails')
58
+
59
+
60
+ def test_eval_happens_before_the_submit():
61
+ """Calling it after forward_async would be too late to matter."""
62
+ node = create_job(REMOTE_DEVICE)
63
+
64
+ eval_at = call_lines(node, 'eval')
65
+ submit_at = call_lines(node, 'forward_async')
66
+
67
+ assert submit_at, 'no forward_async call in _create_job'
68
+ assert min(eval_at) < min(submit_at), (
69
+ f'.eval() is at line {min(eval_at)} but forward_async is at '
70
+ f'{min(submit_at)} -- the model is still in training mode when it is '
71
+ f'submitted')
72
+
73
+
74
+ def test_the_model_that_is_submitted_is_the_one_that_was_evalled():
75
+ """`spec.model.eval()` returns the module, so the submit has to use that
76
+ return value -- passing spec.model again would work only by side effect and
77
+ would break the moment eval() stops returning self."""
78
+ node = create_job(REMOTE_DEVICE)
79
+
80
+ submitted = None
81
+ for inner in ast.walk(node):
82
+ if isinstance(inner, ast.Call) and \
83
+ getattr(inner.func, 'attr', None) == 'forward_async':
84
+ submitted = inner.args[0] if inner.args else None
85
+
86
+ assert isinstance(submitted, ast.Name), (
87
+ 'forward_async should be handed the local name bound from '
88
+ 'spec.model.eval(), not an attribute read straight off the spec')
89
+
90
+
91
+ def test_local_device_also_evals_before_inference():
92
+ """Same rule on the simulator path, where it was already right -- a
93
+ regression there would be just as invisible."""
94
+ assert call_lines(create_job(LOCAL_DEVICE), 'eval')
95
+
96
+
97
+ @pytest.mark.parametrize('path', [REMOTE_DEVICE, LOCAL_DEVICE],
98
+ ids=lambda p: p.name)
99
+ def test_no_train_mode_on_the_inference_path(path):
100
+ """`.train()` belongs to the training loop only. On a device that submits to
101
+ a QPU it would put the module straight back into the state MerLin refuses."""
102
+ node = create_job(path)
103
+
104
+ assert not call_lines(node, 'train'), (
105
+ f'{path.name}: _create_job calls .train(); the training loop is the '
106
+ f'only place that may do that')