quapp-merlin 0.0.1.dev1__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.
- quapp_merlin-0.0.1.dev1/LICENSE +8 -0
- quapp_merlin-0.0.1.dev1/PKG-INFO +138 -0
- quapp_merlin-0.0.1.dev1/README.md +107 -0
- quapp_merlin-0.0.1.dev1/pyproject.toml +43 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/__init__.py +8 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/component/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/component/backend/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/component/backend/merlin_invocation.py +82 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/data/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/data/merlin_job_spec.py +93 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/error_rules.py +190 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/factory/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/factory/merlin_device_factory.py +42 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/factory/merlin_handler_factory.py +35 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/factory/merlin_provider_factory.py +36 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/handler/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/handler/invocation_handler.py +59 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/merlin_device.py +160 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/merlin_local_device.py +195 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/device/merlin_remote_device.py +164 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/provider/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/provider/quandela_merlin_provider.py +57 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/model/provider/quapp_merlin_provider.py +31 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/util/__init__.py +0 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/util/single_machine.py +135 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin/util/tensors.py +127 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/PKG-INFO +138 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/SOURCES.txt +38 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/dependency_links.txt +1 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/requires.txt +9 -0
- quapp_merlin-0.0.1.dev1/quapp_merlin.egg-info/top_level.txt +1 -0
- quapp_merlin-0.0.1.dev1/setup.cfg +4 -0
- quapp_merlin-0.0.1.dev1/tests/test_error_logging.py +304 -0
- quapp_merlin-0.0.1.dev1/tests/test_error_rules.py +181 -0
- quapp_merlin-0.0.1.dev1/tests/test_job_spec.py +94 -0
- quapp_merlin-0.0.1.dev1/tests/test_lazy_sdk_imports.py +93 -0
- quapp_merlin-0.0.1.dev1/tests/test_single_machine.py +214 -0
- quapp_merlin-0.0.1.dev1/tests/test_tensors.py +158 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quapp-merlin
|
|
3
|
+
Version: 0.0.1.dev1
|
|
4
|
+
Summary: Quapp common library supporting Quapp Platform for Quantum Computing
|
|
5
|
+
Author-email: "CITYNOW Co. Ltd. " <corp@citynow.vn>
|
|
6
|
+
License: The MIT License (MIT)
|
|
7
|
+
Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
14
|
+
Project-URL: Homepage, https://quapp.cloud/
|
|
15
|
+
Keywords: quapp,quapp-merlin,quantum,photonic,pytorch
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Requires-Python: <3.15,>=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: quapp-common==0.0.15.dev5
|
|
23
|
+
Requires-Dist: merlinquantum==0.4.1
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black; extra == "dev"
|
|
26
|
+
Requires-Dist: bumpver; extra == "dev"
|
|
27
|
+
Requires-Dist: isort; extra == "dev"
|
|
28
|
+
Requires-Dist: pip-tools; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# quapp-merlin
|
|
33
|
+
|
|
34
|
+
Quapp provider lib for [MerLin](https://merlinquantum.ai/), Quandela's photonic
|
|
35
|
+
quantum machine learning framework. MerLin wraps Perceval but is a separate SDK
|
|
36
|
+
here: a MerLin handler returns a **PyTorch model**, not a circuit.
|
|
37
|
+
|
|
38
|
+
SDK tag: `MERLIN` · providers: `QUAO_QUANTUM_SIMULATOR` (local), `QUANDELA` (cloud)
|
|
39
|
+
|
|
40
|
+
## What a handler returns
|
|
41
|
+
|
|
42
|
+
Every other SDK on the platform has `processing()` return a circuit. MerLin has
|
|
43
|
+
no circuit to return -- the unit of work is a `torch.nn.Module` holding a
|
|
44
|
+
`merlin.QuantumLayer`, plus the batch to run it on.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import merlin as ML
|
|
48
|
+
import torch
|
|
49
|
+
|
|
50
|
+
from quapp_merlin import MerlinJobSpec
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def processing(invocation_input: dict) -> MerlinJobSpec:
|
|
54
|
+
layer = ML.QuantumLayer.simple(
|
|
55
|
+
input_size=4,
|
|
56
|
+
measurement_strategy=ML.MeasurementStrategy.probs())
|
|
57
|
+
|
|
58
|
+
return MerlinJobSpec(model=layer,
|
|
59
|
+
inputs=torch.tensor(invocation_input['inputs']),
|
|
60
|
+
shots=invocation_input.get('shots'))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def post_processing(job_result: dict) -> dict:
|
|
64
|
+
return {'probabilities': job_result['result']['values']}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Returning a bare `torch.nn.Module` also works, and the batch is then read from
|
|
68
|
+
`invocation_input["inputs"]`.
|
|
69
|
+
|
|
70
|
+
`shots=None` (the default) means **exact probabilities**: MerLin does not invent
|
|
71
|
+
a shot count, and neither does this lib.
|
|
72
|
+
|
|
73
|
+
## One invocation, one machine
|
|
74
|
+
|
|
75
|
+
MerLin fans out along three axes, and the first one can put a job on a machine
|
|
76
|
+
nobody selected:
|
|
77
|
+
|
|
78
|
+
| Axis | What it does | Rule here |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Backend | one `MerlinProcessor` binds one backend, but a handler can build a second one and route layers to it | **refused** -- the machine comes from DeviceSelection |
|
|
81
|
+
| Chunks | `len(X) > microbatch_size` splits a batch into several cloud jobs | `chunk_concurrency=1`, batch must fit one microbatch |
|
|
82
|
+
| Leaves | several `QuantumLayer`s in one model, each its own remote job | one layer per invocation |
|
|
83
|
+
|
|
84
|
+
Together these keep `future.job_ids` a single id, so `provider_job_id` stays one
|
|
85
|
+
value the backend can store as-is. The rules live in
|
|
86
|
+
`quapp_merlin/util/single_machine.py` and are locked by
|
|
87
|
+
`tests/test_single_machine.py`.
|
|
88
|
+
|
|
89
|
+
## Local vs remote
|
|
90
|
+
|
|
91
|
+
| | local (`QUAO_QUANTUM_SIMULATOR`) | remote (`QUANDELA`) |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| Runs on | SLOS simulation in the container, **CPU only** | Quandela Cloud, `sim:*` or `qpu:*` |
|
|
94
|
+
| Training | yes, `MerlinJobSpec(training=TrainingSpec(...))` | **no** -- no gradient flows through the MerLin processor |
|
|
95
|
+
| Phases | COMPILATION, EXECUTION, ANALYSIS | plus POLLING while waiting on the cloud |
|
|
96
|
+
| Job id | none | one, from `future.job_ids` |
|
|
97
|
+
|
|
98
|
+
The image ships the CPU build of torch on purpose: the default wheel drags in
|
|
99
|
+
the whole CUDA runtime (~2.5 GB) for a photonic simulator that never touches a
|
|
100
|
+
GPU. A job asking for `ProcessingUnit.GPU` runs on CPU with a warning.
|
|
101
|
+
|
|
102
|
+
## Result shape
|
|
103
|
+
|
|
104
|
+
`_create_job` returns a dict that is already JSON-safe -- no tensor ever reaches
|
|
105
|
+
`json.dumps` in a callback:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
{'result': {'values': [[0.24, 0.76]], 'truncated': False, 'elements': 2},
|
|
109
|
+
'shape': [1, 2],
|
|
110
|
+
'batch_size': 1,
|
|
111
|
+
'shots': None,
|
|
112
|
+
'measurement': 'probs',
|
|
113
|
+
'execution_time': 0.184,
|
|
114
|
+
'provider_job_id': None,
|
|
115
|
+
'histogram': {'state_0': 0.24, 'state_1': 0.76},
|
|
116
|
+
'metadata': {}}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`histogram` is only produced for a probability measurement of a single row: with
|
|
120
|
+
a batch there are as many distributions as rows, and with
|
|
121
|
+
`mode_expectations()` / `amplitudes()` there is no distribution over states to
|
|
122
|
+
bin. It is `None` in those cases, and the full output stays in `result`.
|
|
123
|
+
|
|
124
|
+
Large outputs are truncated with `truncated: True` and the real element count,
|
|
125
|
+
rather than silently sending the first rows as if they were everything.
|
|
126
|
+
|
|
127
|
+
## Versions
|
|
128
|
+
|
|
129
|
+
| | |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `merlinquantum` | `==0.4.1` -- 0.4 renamed the `QuantumLayer` arguments 0.3 used (`output_mapping_strategy` -> `measurement_strategy`, `ansatz` -> `builder`), so `>=` would break every handler |
|
|
132
|
+
| `quapp-common` | `==0.0.15.dev5` -- first release carrying `Sdk.MERLIN` |
|
|
133
|
+
| Python | `>=3.10` (merlinquantum), `<3.15` (perceval-quandela) |
|
|
134
|
+
|
|
135
|
+
`import quapp_merlin` needs none of torch, merlin or perceval: every SDK import
|
|
136
|
+
sits inside the function that uses it, so the error classifier still works in an
|
|
137
|
+
image where the SDK failed to install. `tests/test_lazy_sdk_imports.py` enforces
|
|
138
|
+
it.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# quapp-merlin
|
|
2
|
+
|
|
3
|
+
Quapp provider lib for [MerLin](https://merlinquantum.ai/), Quandela's photonic
|
|
4
|
+
quantum machine learning framework. MerLin wraps Perceval but is a separate SDK
|
|
5
|
+
here: a MerLin handler returns a **PyTorch model**, not a circuit.
|
|
6
|
+
|
|
7
|
+
SDK tag: `MERLIN` · providers: `QUAO_QUANTUM_SIMULATOR` (local), `QUANDELA` (cloud)
|
|
8
|
+
|
|
9
|
+
## What a handler returns
|
|
10
|
+
|
|
11
|
+
Every other SDK on the platform has `processing()` return a circuit. MerLin has
|
|
12
|
+
no circuit to return -- the unit of work is a `torch.nn.Module` holding a
|
|
13
|
+
`merlin.QuantumLayer`, plus the batch to run it on.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import merlin as ML
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
from quapp_merlin import MerlinJobSpec
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def processing(invocation_input: dict) -> MerlinJobSpec:
|
|
23
|
+
layer = ML.QuantumLayer.simple(
|
|
24
|
+
input_size=4,
|
|
25
|
+
measurement_strategy=ML.MeasurementStrategy.probs())
|
|
26
|
+
|
|
27
|
+
return MerlinJobSpec(model=layer,
|
|
28
|
+
inputs=torch.tensor(invocation_input['inputs']),
|
|
29
|
+
shots=invocation_input.get('shots'))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def post_processing(job_result: dict) -> dict:
|
|
33
|
+
return {'probabilities': job_result['result']['values']}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Returning a bare `torch.nn.Module` also works, and the batch is then read from
|
|
37
|
+
`invocation_input["inputs"]`.
|
|
38
|
+
|
|
39
|
+
`shots=None` (the default) means **exact probabilities**: MerLin does not invent
|
|
40
|
+
a shot count, and neither does this lib.
|
|
41
|
+
|
|
42
|
+
## One invocation, one machine
|
|
43
|
+
|
|
44
|
+
MerLin fans out along three axes, and the first one can put a job on a machine
|
|
45
|
+
nobody selected:
|
|
46
|
+
|
|
47
|
+
| Axis | What it does | Rule here |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Backend | one `MerlinProcessor` binds one backend, but a handler can build a second one and route layers to it | **refused** -- the machine comes from DeviceSelection |
|
|
50
|
+
| Chunks | `len(X) > microbatch_size` splits a batch into several cloud jobs | `chunk_concurrency=1`, batch must fit one microbatch |
|
|
51
|
+
| Leaves | several `QuantumLayer`s in one model, each its own remote job | one layer per invocation |
|
|
52
|
+
|
|
53
|
+
Together these keep `future.job_ids` a single id, so `provider_job_id` stays one
|
|
54
|
+
value the backend can store as-is. The rules live in
|
|
55
|
+
`quapp_merlin/util/single_machine.py` and are locked by
|
|
56
|
+
`tests/test_single_machine.py`.
|
|
57
|
+
|
|
58
|
+
## Local vs remote
|
|
59
|
+
|
|
60
|
+
| | local (`QUAO_QUANTUM_SIMULATOR`) | remote (`QUANDELA`) |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Runs on | SLOS simulation in the container, **CPU only** | Quandela Cloud, `sim:*` or `qpu:*` |
|
|
63
|
+
| Training | yes, `MerlinJobSpec(training=TrainingSpec(...))` | **no** -- no gradient flows through the MerLin processor |
|
|
64
|
+
| Phases | COMPILATION, EXECUTION, ANALYSIS | plus POLLING while waiting on the cloud |
|
|
65
|
+
| Job id | none | one, from `future.job_ids` |
|
|
66
|
+
|
|
67
|
+
The image ships the CPU build of torch on purpose: the default wheel drags in
|
|
68
|
+
the whole CUDA runtime (~2.5 GB) for a photonic simulator that never touches a
|
|
69
|
+
GPU. A job asking for `ProcessingUnit.GPU` runs on CPU with a warning.
|
|
70
|
+
|
|
71
|
+
## Result shape
|
|
72
|
+
|
|
73
|
+
`_create_job` returns a dict that is already JSON-safe -- no tensor ever reaches
|
|
74
|
+
`json.dumps` in a callback:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
{'result': {'values': [[0.24, 0.76]], 'truncated': False, 'elements': 2},
|
|
78
|
+
'shape': [1, 2],
|
|
79
|
+
'batch_size': 1,
|
|
80
|
+
'shots': None,
|
|
81
|
+
'measurement': 'probs',
|
|
82
|
+
'execution_time': 0.184,
|
|
83
|
+
'provider_job_id': None,
|
|
84
|
+
'histogram': {'state_0': 0.24, 'state_1': 0.76},
|
|
85
|
+
'metadata': {}}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`histogram` is only produced for a probability measurement of a single row: with
|
|
89
|
+
a batch there are as many distributions as rows, and with
|
|
90
|
+
`mode_expectations()` / `amplitudes()` there is no distribution over states to
|
|
91
|
+
bin. It is `None` in those cases, and the full output stays in `result`.
|
|
92
|
+
|
|
93
|
+
Large outputs are truncated with `truncated: True` and the real element count,
|
|
94
|
+
rather than silently sending the first rows as if they were everything.
|
|
95
|
+
|
|
96
|
+
## Versions
|
|
97
|
+
|
|
98
|
+
| | |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `merlinquantum` | `==0.4.1` -- 0.4 renamed the `QuantumLayer` arguments 0.3 used (`output_mapping_strategy` -> `measurement_strategy`, `ansatz` -> `builder`), so `>=` would break every handler |
|
|
101
|
+
| `quapp-common` | `==0.0.15.dev5` -- first release carrying `Sdk.MERLIN` |
|
|
102
|
+
| Python | `>=3.10` (merlinquantum), `<3.15` (perceval-quandela) |
|
|
103
|
+
|
|
104
|
+
`import quapp_merlin` needs none of torch, merlin or perceval: every SDK import
|
|
105
|
+
sits inside the function that uses it, so the error classifier still works in an
|
|
106
|
+
image where the SDK failed to install. `tests/test_lazy_sdk_imports.py` enforces
|
|
107
|
+
it.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel==0.45.1"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "quapp-merlin"
|
|
7
|
+
version = "0.0.1.dev1"
|
|
8
|
+
description = "Quapp common library supporting Quapp Platform for Quantum Computing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "CITYNOW Co. Ltd. ", email = "corp@citynow.vn" }]
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["quapp", "quapp-merlin", "quantum", "photonic", "pytorch"]
|
|
18
|
+
# merlinquantum 0.4 renamed the QuantumLayer arguments that 0.3 used
|
|
19
|
+
# (output_mapping_strategy -> measurement_strategy, ansatz -> builder), so `>=`
|
|
20
|
+
# would silently break every handler. Exact pin, same policy as the other libs.
|
|
21
|
+
#
|
|
22
|
+
# torch is NOT listed here on purpose: the base image installs the CPU wheel
|
|
23
|
+
# from download.pytorch.org, and naming it here would let pip pull the default
|
|
24
|
+
# wheel instead, which drags in the whole CUDA runtime (~2.5 GB) for a photonic
|
|
25
|
+
# QPU that never touches a GPU. merlinquantum already declares torch, so the
|
|
26
|
+
# constraint is still enforced -- it is just satisfied by what the image
|
|
27
|
+
# installed first.
|
|
28
|
+
dependencies = [
|
|
29
|
+
# 0.0.15.dev5 is the first release carrying Sdk.MERLIN.
|
|
30
|
+
"quapp-common==0.0.15.dev5",
|
|
31
|
+
"merlinquantum==0.4.1"
|
|
32
|
+
]
|
|
33
|
+
# merlinquantum requires >=3.10; perceval-quandela caps at <3.15.
|
|
34
|
+
requires-python = ">=3.10,<3.15"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["black", "bumpver", "isort", "pip-tools", "pytest"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://quapp.cloud/"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
include = ["quapp_merlin*"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .data.merlin_job_spec import MerlinJobSpec, TrainingSpec
|
|
2
|
+
from .factory.merlin_handler_factory import MerlinHandlerFactory
|
|
3
|
+
|
|
4
|
+
# Imported for the side effect: register_rules() has to run before the
|
|
5
|
+
# first job fails, so this import cannot be made lazy.
|
|
6
|
+
from . import error_rules as _error_rules # noqa: E402,F401
|
|
7
|
+
|
|
8
|
+
__all__ = ['MerlinJobSpec', 'TrainingSpec', 'MerlinHandlerFactory']
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# merlin_invocation.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""Wiring MerLin into the invocation pipeline.
|
|
6
|
+
|
|
7
|
+
The pipeline carries whatever ``processing()`` returned without looking at it --
|
|
8
|
+
except in one place, ``_get_qubit_amount``, which DeviceSelection uses to pick a
|
|
9
|
+
machine. That is the only spot where a MerLin job has to explain itself, and the
|
|
10
|
+
answer is a **mode count**, not a qubit count: see the note on the method.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from quapp_common.component.backend.invocation import Invocation
|
|
14
|
+
from quapp_common.data.request.invocation_request import InvocationRequest
|
|
15
|
+
from quapp_common.model.provider.provider import Provider
|
|
16
|
+
|
|
17
|
+
from ...factory.merlin_device_factory import MerlinDeviceFactory
|
|
18
|
+
from ...factory.merlin_provider_factory import MerlinProviderFactory
|
|
19
|
+
from ...util.single_machine import quantum_leaves
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MerlinInvocation(Invocation):
|
|
23
|
+
|
|
24
|
+
def __init__(self, request_data: InvocationRequest):
|
|
25
|
+
super().__init__(request_data)
|
|
26
|
+
|
|
27
|
+
def _export_circuit(self, circuit):
|
|
28
|
+
# Only QISKIT and BRAKET export circuits (EXPORT_CIRCUIT_SDK in
|
|
29
|
+
# quapp-common). A MerLin job is a torch module, not a drawable circuit.
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def _create_provider(self):
|
|
33
|
+
self.logger.debug(
|
|
34
|
+
f'Creating provider: '
|
|
35
|
+
f'provider_tag={getattr(self.backend_information, "provider_tag", None)}')
|
|
36
|
+
provider = MerlinProviderFactory.create_provider(
|
|
37
|
+
provider_type=self.backend_information.provider_tag,
|
|
38
|
+
sdk=self.sdk,
|
|
39
|
+
authentication=self.backend_information.authentication)
|
|
40
|
+
self.logger.info(f'Provider created: {type(provider).__name__}')
|
|
41
|
+
return provider
|
|
42
|
+
|
|
43
|
+
def _create_device(self, provider: Provider):
|
|
44
|
+
device_specification = getattr(self.backend_information, 'device_name',
|
|
45
|
+
None)
|
|
46
|
+
self.logger.debug(f'Creating device: {device_specification}')
|
|
47
|
+
device = MerlinDeviceFactory.create_device(
|
|
48
|
+
provider=provider,
|
|
49
|
+
device_specification=device_specification,
|
|
50
|
+
authentication=self.backend_information.authentication,
|
|
51
|
+
sdk=self.sdk)
|
|
52
|
+
self.logger.info(f'Device created: {type(device).__name__}')
|
|
53
|
+
return device
|
|
54
|
+
|
|
55
|
+
def _get_qubit_amount(self, circuit):
|
|
56
|
+
"""How many optical **modes** the job needs.
|
|
57
|
+
|
|
58
|
+
DeviceSelection asks for a qubit count because every other SDK is
|
|
59
|
+
gate-based. Photonic hardware is sized in modes, and MerLin's layers
|
|
60
|
+
carry a Perceval circuit whose ``m`` is exactly that. Reporting modes
|
|
61
|
+
here is the closest true answer; a device catalogue that stores qubits
|
|
62
|
+
for a Quandela machine has to be read the same way.
|
|
63
|
+
|
|
64
|
+
Returns 0 rather than raising when the model hides its circuit: failing
|
|
65
|
+
device selection over a metric nobody uses downstream would turn a
|
|
66
|
+
runnable job into a failed one.
|
|
67
|
+
"""
|
|
68
|
+
for _, leaf in quantum_leaves(getattr(circuit, 'model', circuit)):
|
|
69
|
+
for attribute in ('m', 'n_modes', 'modes'):
|
|
70
|
+
modes = getattr(leaf, attribute, None)
|
|
71
|
+
if isinstance(modes, int) and modes > 0:
|
|
72
|
+
return modes
|
|
73
|
+
|
|
74
|
+
inner = getattr(leaf, 'circuit', None)
|
|
75
|
+
modes = getattr(inner, 'm', None)
|
|
76
|
+
if isinstance(modes, int) and modes > 0:
|
|
77
|
+
return modes
|
|
78
|
+
|
|
79
|
+
self.logger.warning(
|
|
80
|
+
'Could not read a mode count from the model; reporting 0 to '
|
|
81
|
+
'device selection.')
|
|
82
|
+
return 0
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Quapp Platform Project
|
|
2
|
+
# merlin_job_spec.py
|
|
3
|
+
# Copyright © CITYNOW Co. Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
"""What a MerLin handler returns instead of a circuit.
|
|
6
|
+
|
|
7
|
+
Every other SDK in the platform has ``processing()`` return a circuit, and the
|
|
8
|
+
whole pipeline carries that object around opaquely. MerLin has no circuit to
|
|
9
|
+
return: the unit of work is a ``torch.nn.Module`` (normally one holding an
|
|
10
|
+
``merlin.QuantumLayer``) plus the input tensor to run it on.
|
|
11
|
+
|
|
12
|
+
Returning a bare Module would not be enough -- the model alone says nothing about
|
|
13
|
+
WHAT to run it on, and the device would have to guess which key of
|
|
14
|
+
``invocation_input`` holds the data. A spec makes the contract explicit and lets
|
|
15
|
+
the device reject a malformed job with a specific error code instead of a
|
|
16
|
+
``TypeError`` deep inside torch.
|
|
17
|
+
|
|
18
|
+
A bare Module is still accepted as shorthand; see ``coerce()``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TrainingSpec:
|
|
26
|
+
"""Local-only training.
|
|
27
|
+
|
|
28
|
+
MerLin's remote processor cannot backpropagate -- the documentation is
|
|
29
|
+
explicit: *"No gradient can flow through the MerLin processor."* So a spec
|
|
30
|
+
carrying ``training`` is refused on a remote device and only runs on the
|
|
31
|
+
local simulator.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
targets: object
|
|
35
|
+
epochs: int = 10
|
|
36
|
+
learning_rate: float = 0.01
|
|
37
|
+
loss: str = 'mse'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class MerlinJobSpec:
|
|
42
|
+
"""One MerLin job: one model, one input batch, one machine.
|
|
43
|
+
|
|
44
|
+
@param model: the torch Module to run. Must contain exactly one quantum
|
|
45
|
+
layer -- see the single-machine rules in
|
|
46
|
+
``model/device/merlin_remote_device.py``.
|
|
47
|
+
@param inputs: the input batch, shape ``[batch, input_size]``. Anything
|
|
48
|
+
``torch.as_tensor`` accepts is fine (list, ndarray, tensor).
|
|
49
|
+
@param shots: sample count. ``None`` or ``0`` means exact probabilities --
|
|
50
|
+
MerLin does not invent a default, and neither do we.
|
|
51
|
+
@param sampling_method: e.g. ``'multinomial'``; ``None`` leaves MerLin's
|
|
52
|
+
default alone.
|
|
53
|
+
@param training: present only for local training runs.
|
|
54
|
+
@param metadata: free-form, copied into the job result for the handler's own
|
|
55
|
+
post_processing().
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
model: object
|
|
59
|
+
inputs: object
|
|
60
|
+
shots: int = None
|
|
61
|
+
sampling_method: str = None
|
|
62
|
+
training: TrainingSpec = None
|
|
63
|
+
metadata: dict = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
SPEC_HELP = (
|
|
67
|
+
'processing() must return a MerlinJobSpec, or a torch.nn.Module when the '
|
|
68
|
+
'input batch is in invocation_input["inputs"].')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def coerce(prepared, invocation_input: dict = None) -> MerlinJobSpec:
|
|
72
|
+
"""Turn what ``processing()`` returned into a MerlinJobSpec.
|
|
73
|
+
|
|
74
|
+
A bare Module is accepted because the common case -- one layer, data already
|
|
75
|
+
in the request -- should not need the import. The data then has to be in
|
|
76
|
+
``invocation_input["inputs"]``; anything else is rejected here rather than
|
|
77
|
+
surfacing later as a shape error inside torch.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(prepared, MerlinJobSpec):
|
|
80
|
+
return prepared
|
|
81
|
+
|
|
82
|
+
if hasattr(prepared, 'forward') and callable(prepared):
|
|
83
|
+
inputs = (invocation_input or {}).get('inputs')
|
|
84
|
+
if inputs is None:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
'processing() returned a model but no input batch: put it '
|
|
87
|
+
'in invocation_input["inputs"], or return a MerlinJobSpec '
|
|
88
|
+
'with inputs=... instead.')
|
|
89
|
+
return MerlinJobSpec(model=prepared, inputs=inputs,
|
|
90
|
+
shots=(invocation_input or {}).get('shots'))
|
|
91
|
+
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f'processing() returned {type(prepared).__name__}. {SPEC_HELP}')
|