ilang-python 0.1.0__py3-none-macosx_11_0_arm64.whl

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.
ilang/inputs.py ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ from typing import Any
5
+
6
+ from . import ffi
7
+ from .tensor import Device, Tensor, _shape_array
8
+
9
+ __all__: list[str] = []
10
+
11
+
12
+ class _Input:
13
+ def __init__(self, tensor: ffi._CTensor, keepalive: tuple[Any, ...]) -> None:
14
+ self.tensor: ffi._CTensor = tensor
15
+ self.keepalive: tuple[Any, ...] = keepalive
16
+
17
+
18
+ def _input(x: Any) -> _Input:
19
+ if isinstance(x, Tensor):
20
+ try:
21
+ import torch
22
+
23
+ if isinstance(x._owner, torch.Tensor):
24
+ inner = _input(x._owner)
25
+ return _Input(inner.tensor, (x, inner))
26
+ except ImportError:
27
+ pass
28
+ return _Input(x._view(), (x,))
29
+
30
+ try:
31
+ import numpy as np
32
+
33
+ if isinstance(x, np.ndarray):
34
+ if x.dtype != np.float32 or not x.flags.c_contiguous:
35
+ raise TypeError("NumPy inputs must be float32 and C-contiguous")
36
+ shape, shape_buf = _shape_array(x.shape)
37
+ data = x.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
38
+ return _Input(ffi._CTensor(data, shape_buf, len(shape)), (x, shape_buf))
39
+ except ImportError:
40
+ pass
41
+
42
+ try:
43
+ import torch
44
+
45
+ if isinstance(x, torch.Tensor):
46
+ if str(x.dtype) != "torch.float32":
47
+ raise TypeError("Torch tensors must be float32")
48
+ if not x.is_contiguous():
49
+ raise TypeError("Torch tensors must be contiguous")
50
+ shape, shape_buf = _shape_array(tuple(x.shape))
51
+ data = ctypes.cast(x.data_ptr(), ctypes.POINTER(ctypes.c_float))
52
+ return _Input(ffi._CTensor(data, shape_buf, len(shape)), (x, shape_buf))
53
+ except ImportError:
54
+ pass
55
+
56
+ return _input(Tensor(x))
57
+
58
+
59
+ def _inputs(
60
+ xs: list[Any] | tuple[Any, ...],
61
+ ) -> tuple[ctypes.Array[ffi._CTensor], list[_Input]]:
62
+ views: list[_Input] = [_input(x) for x in xs]
63
+ arr: ctypes.Array[ffi._CTensor] = (ffi._CTensor * len(views))(
64
+ *(v.tensor for v in views)
65
+ )
66
+ return arr, views
ilang/libi_core.dylib ADDED
Binary file
ilang/tensor.py ADDED
@@ -0,0 +1,205 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ from enum import Enum
5
+ from numbers import Real
6
+ from typing import Any
7
+
8
+ from . import ffi
9
+
10
+ __all__ = ["Device", "Tensor"]
11
+
12
+
13
+
14
+ class Device(Enum):
15
+ CPU = "cpu"
16
+ CUDA = "cuda"
17
+
18
+ def _as_ffi(self) -> int:
19
+ return 0 if self is Device.CPU else 1
20
+
21
+ @classmethod
22
+ def coerce(cls, value: Device | str) -> Device:
23
+ if isinstance(value, Device):
24
+ return value
25
+ name = str(value).lower()
26
+ if name in {"cpu", "device.cpu"}:
27
+ return Device.CPU
28
+ if name in {"cuda", "gpu", "device.cuda"}:
29
+ return Device.CUDA
30
+ raise ValueError(f"unknown device {value!r}")
31
+
32
+
33
+ def _shape_array(
34
+ shape: tuple[int, ...],
35
+ ) -> tuple[tuple[int, ...], Any]:
36
+ shape = tuple(int(d) for d in shape)
37
+ arr: Any = (ctypes.c_size_t * len(shape))(*shape)
38
+ return shape, arr
39
+
40
+
41
+ def _flatten(x: Any) -> tuple[tuple[int, ...], list[float]]:
42
+ if isinstance(x, Real):
43
+ return (), [float(x)]
44
+ if not isinstance(x, (list, tuple)):
45
+ raise TypeError("Tensor expects a scalar or nested Python lists")
46
+ if not x:
47
+ return (0,), []
48
+
49
+ child_shape, _data = _flatten(x[0])
50
+ shape: tuple[int, ...] = (len(x),) + child_shape
51
+ out: list[float] = []
52
+ for item in x:
53
+ item_shape, item_data = _flatten(item)
54
+ if item_shape != child_shape:
55
+ raise ValueError("ragged Tensor input")
56
+ out.extend(item_data)
57
+ return shape, out
58
+
59
+
60
+ def _torch_cuda_empty(shape: tuple[int, ...]) -> Any | None:
61
+ try:
62
+ import torch
63
+
64
+ if torch.cuda.is_available():
65
+ return torch.empty(shape, dtype=torch.float32, device="cuda")
66
+ except ImportError:
67
+ pass
68
+ return None
69
+
70
+
71
+ def _numel(shape: tuple[int, ...]) -> int:
72
+ n = 1
73
+ for dim in shape:
74
+ n *= dim
75
+ return n
76
+
77
+
78
+ class _OwnedOutputs:
79
+ def __init__(self, outputs: ctypes.Structure) -> None:
80
+ self.outputs: ctypes.Structure | None = outputs
81
+
82
+ def __del__(self) -> None:
83
+ outputs = getattr(self, "outputs", None)
84
+ if outputs is not None:
85
+ self.outputs = None
86
+ ffi._core.i_outputs_free(outputs)
87
+
88
+
89
+ class _DeviceOwner:
90
+ def __init__(self, device: Device, data: Any) -> None:
91
+ self.device = device
92
+ self.data: Any | None = data
93
+
94
+ def __del__(self) -> None:
95
+ data = getattr(self, "data", None)
96
+ if data is not None:
97
+ self.data = None
98
+ ffi._core.i_free(self.device._as_ffi(), data)
99
+
100
+
101
+ class Tensor:
102
+ def __init__(
103
+ self,
104
+ x: Any,
105
+ shape: tuple[int, ...] | None = None,
106
+ *,
107
+ device: Device | str = Device.CPU,
108
+ ) -> None:
109
+ device = Device.coerce(device)
110
+ if shape is None:
111
+ shape, data = _flatten(x)
112
+ else:
113
+ shape = tuple(int(d) for d in shape)
114
+ data = [float(v) for v in x]
115
+ self.shape: tuple[int, ...] = tuple(shape)
116
+ self.device: Device = Device.CPU
117
+ self._len: int = len(data)
118
+ self._data: Any = (ctypes.c_float * self._len)(*data)
119
+ self._shape, self._shape_buf = _shape_array(self.shape)
120
+ self._owner: _OwnedOutputs | _DeviceOwner | None = None
121
+ if device is Device.CUDA:
122
+ moved = self.to(Device.CUDA)
123
+ self.device = moved.device
124
+ self._data = moved._data
125
+ self._owner = moved._owner
126
+ moved._owner = None
127
+
128
+ @classmethod
129
+ def _from_owned(cls, owner: _OwnedOutputs, index: int) -> Tensor:
130
+ outputs = owner.outputs
131
+ assert outputs is not None
132
+ raw = outputs.tensors[index]
133
+ self: Tensor = cls.__new__(cls)
134
+ self.shape = tuple(raw.shape[i] for i in range(raw.rank))
135
+ self.device = Device.CPU
136
+ self._len = raw.len
137
+ self._data = raw.data
138
+ self._shape, self._shape_buf = _shape_array(self.shape)
139
+ self._owner = owner
140
+ return self
141
+
142
+ @classmethod
143
+ def _empty(cls, shape: tuple[int, ...], device: Device | str) -> Tensor:
144
+ device = Device.coerce(device)
145
+ self: Tensor = cls.__new__(cls)
146
+ self.shape = tuple(int(d) for d in shape)
147
+ self.device = device
148
+ self._len = _numel(self.shape)
149
+ self._shape, self._shape_buf = _shape_array(self.shape)
150
+ if device is Device.CPU:
151
+ self._data = (ctypes.c_float * self._len)()
152
+ self._owner = None
153
+ else:
154
+ torch_owner = _torch_cuda_empty(self.shape)
155
+ if torch_owner is not None:
156
+ self._data = ctypes.cast(torch_owner.data_ptr(), ctypes.POINTER(ctypes.c_float))
157
+ self._owner = torch_owner
158
+ else:
159
+ data = ffi._check_ptr(ffi._core.i_alloc(device._as_ffi(), self._len))
160
+ self._data = ctypes.cast(data, ctypes.POINTER(ctypes.c_float))
161
+ self._owner = _DeviceOwner(device, self._data)
162
+ return self
163
+
164
+ @property
165
+ def data(self) -> list[float]:
166
+ if self.device is not Device.CPU:
167
+ raise RuntimeError(
168
+ "CUDA tensor data is not directly accessible; call .to(Device.CPU) first"
169
+ )
170
+ return [self._data[i] for i in range(self._len)]
171
+
172
+ def to(self, device: Device | str) -> Tensor:
173
+ device = Device.coerce(device)
174
+ if device is self.device:
175
+ return self
176
+ out = Tensor._empty(self.shape, device)
177
+ ffi._check(
178
+ ffi._core.i_copy(
179
+ out.device._as_ffi(),
180
+ out._data,
181
+ self.device._as_ffi(),
182
+ self._data,
183
+ self._len,
184
+ )
185
+ )
186
+ return out
187
+
188
+ def _view(self) -> ffi._CTensor:
189
+ try:
190
+ import torch
191
+
192
+ if isinstance(self._owner, torch.Tensor):
193
+ data = ctypes.cast(self._owner.data_ptr(), ctypes.POINTER(ctypes.c_float))
194
+ return ffi._CTensor(data, self._shape_buf, len(self.shape))
195
+ except ImportError:
196
+ pass
197
+ return ffi._CTensor(self._data, self._shape_buf, len(self.shape))
198
+
199
+ def __del__(self) -> None:
200
+ self._owner = None
201
+
202
+ def __repr__(self) -> str:
203
+ if self.device is Device.CPU:
204
+ return f"Tensor(shape={self.shape}, device=CPU, data={self.data})"
205
+ return f"Tensor(shape={self.shape}, device=CUDA)"
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: ilang-python
3
+ Version: 0.1.0
4
+ Summary: Python front-end for 𝚒
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE
7
+ Classifier: License :: OSI Approved :: Apache Software License
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ Python front-end for 𝚒.
12
+
13
+ This package exposes 𝚒 components as Python objects. Components compile lazily,
14
+ execute on CPU or CUDA according to their inputs, and interoperate with
15
+ `i.Tensor`, Python scalar/list literals, NumPy `array`s, and Torch `tensor`s.
16
+
17
+ ## Package-style API
18
+
19
+ ```python
20
+ import ilang
21
+ ```
22
+
23
+ Exports:
24
+
25
+ - `ilang.Component`
26
+ - `ilang.Tensor`
27
+ - `ilang.Device`
28
+ - `ilang.Bench`
29
+ - `ilang.i`
30
+
31
+ ## Preferred "DSL-style" API
32
+
33
+ The package-exported object `i` acts as a callable "namespace" that enables a
34
+ more compact style of 𝚒 code. When called, it constructs a `Component`, but
35
+ it also re-exposes much of the same package-level API as attributes.
36
+
37
+ ```python
38
+ from ilang import i
39
+
40
+ i("+i~.") # <ilang.component.Component object at ...>
41
+ i.Tensor # <class 'ilang.tensor.Tensor'>
42
+ i.Component # <class 'ilang.component.Component'>
43
+ i.Device # <enum 'Device'>
44
+ i.I # mirrors `ilang.Component.I`
45
+ ```
46
+
47
+ ## Devices
48
+
49
+ A `Device` dictates where data lives and where compuation will run.
50
+
51
+ ```python
52
+ i.Device.CPU # "cpu"
53
+ i.Device.CUDA # "cuda"
54
+ ```
55
+
56
+ ## Tensors
57
+
58
+ `Tensor`s are immutable multidimensional data arrays.
59
+
60
+ ```python
61
+ x = i.Tensor([[1, 2], [3, 4]]) # standard construction does shape-inference on nested list
62
+ x = i.Tensor([1, 2, 3, 4], shape=(2, 2)) # flat data with shape also works
63
+ ```
64
+
65
+ Values are stored as `float32`.
66
+
67
+ ### Attributes
68
+
69
+ ```python
70
+ x.shape # tuple[int, ...]
71
+ x.device # i.Device.CPU or i.Device.CUDA
72
+ x.data # list[float] (only available on CPU tensors)
73
+ ```
74
+
75
+ ### Methods
76
+
77
+ ```python
78
+ Tensor.to(device i.Device) -> Tensor # gives a new tensor on specified device
79
+
80
+ # examples:
81
+ x.to(i.Device.CUDA)
82
+ x.to(i.Device.CPU)
83
+ x.to("cuda")
84
+ x.to("cpu")
85
+ ```
86
+
87
+ ## Components
88
+
89
+ `i(expr: str) -> Component` parses one 𝚒 expression.
90
+
91
+ ```python
92
+ f = i("+ij~i") # row-sum
93
+ ```
94
+
95
+ `i.I` is the identity component.
96
+
97
+ ### Component combinators
98
+
99
+ Components are combined using combinators. Components are immutable, so
100
+ combinators each return a new component.
101
+
102
+ Method forms:
103
+
104
+ ```python
105
+ f.compose(g) # wires outputs of the right component into inputs of the left component
106
+ f.chain(g) # wires outputs of the left component into inputs of the right component
107
+ f.fanout(g) # shares inputs pairwise between two components
108
+ f.pair(g) # concatenates the inputs and outputs of two components
109
+ f.swap() # swaps the first two outputs of one component
110
+ ```
111
+
112
+ Operator forms:
113
+
114
+ ```python
115
+ f << g # f.compose(g)
116
+ f >> g # f.chain(g)
117
+ f & g # f.fanout(g)
118
+ f | g # f.pair(g)
119
+ ~f # f.swap()
120
+ ```
121
+
122
+ Example:
123
+
124
+ ```python
125
+ matmul = i("ik*kj~ijk") >> i("+ijk~ij")
126
+ ```
127
+
128
+ ## Execution
129
+
130
+ ```python
131
+ out = matmul.exec(x, y)
132
+ ```
133
+
134
+ `Component.exec(*inputs: TensorLike, into=None)` executes one component.
135
+
136
+ where `TensorLike = Tensor | torch.Tensor | numpy.ndarray | nested Python
137
+ sequence`
138
+
139
+ Execution device is determined by the input devices. All inputs must use the
140
+ same device. NumPy/Torch inputs must be contiguous and of dtype `float32`.
141
+ Python scalars/lists get promoted to CPU `ilang.Tensor`.
142
+
143
+ ### Output type inference
144
+
145
+ The output container type and device are inferred from the input. For example,
146
+ `f.exec(torch.tensor([...], device="cuda"))` returns an `torch.Tensor` with
147
+ `device="cuda"`. This can be overridden with `into=`: `f.exec(nparray,
148
+ into=i.Tensor)`.
149
+
150
+ In general, component execution returns `Tuple[Tensor]` but is automatically
151
+ unpacked for single output results.
152
+
153
+ ### Shape inference
154
+
155
+ `Component.output_shapes(*inputs)` returns one shape tuple per output. Shapes
156
+ are computed from input shapes without executing any kernels.
157
+
158
+ ## Benchmarking
159
+
160
+ ```python
161
+ bench = f.bench([x], n_warmups=10, n_runs=100)
162
+ ```
163
+
164
+ `bench` executes warmup runs, records timed runs, and returns `Bench`.
165
+
166
+ `Bench` fields:
167
+
168
+ ```python
169
+ bench.mean # datetime.timedelta
170
+ bench.std # datetime.timedelta
171
+ bench.n_warmups # int
172
+ bench.n_runs # int
173
+ bench.runs # list[datetime.timedelta]
174
+ ```
175
+
176
+ `repr(bench)` prints a compact human-readable timing summary.
177
+
178
+ ## Errors
179
+
180
+ Invalid programs, invalid input types, invalid dtypes, non-contiguous arrays,
181
+ device mismatches, and backend failures raise Python exceptions.
182
+
183
+ Execution requires all inputs to reside on one device. No implicit CPU/CUDA
184
+ input synchronization is performed by `exec`.
185
+
186
+ CUDA tensor data is not read by `repr` and is not exposed by `.data`. Copy to
187
+ CPU explicitly.
188
+
189
+ ## Examples
190
+
191
+ Native tensor execution:
192
+
193
+ ```python
194
+ from ilang import i
195
+
196
+ f = i.i("+ij~ij")
197
+ x = i.Tensor([[1, 2], [3, 4]])
198
+ y = f.exec(x)
199
+ ```
200
+
201
+ CUDA tensor execution:
202
+
203
+ ```python
204
+ x = i.Tensor([[1, 2], [3, 4]]).to("cuda")
205
+ y = f.exec(x)
206
+ z = y.to("cpu")
207
+ ```
208
+
209
+ NumPy execution:
210
+
211
+ ```python
212
+ import numpy as np
213
+
214
+ x = np.ones((2, 2), dtype=np.float32)
215
+ y = f.exec(x)
216
+ ```
217
+
218
+ Torch CUDA execution:
219
+
220
+ ```python
221
+ import torch
222
+
223
+ x = torch.ones((2, 2), dtype=torch.float32, device="cuda")
224
+ y = f.exec(x)
225
+ ```
@@ -0,0 +1,10 @@
1
+ ilang/__init__.py,sha256=mPPSerGXB03-ZnBw73WoAxCr6D8oGOXkpQS3MEbP8Us,407
2
+ ilang/component.py,sha256=Eyj2woo0CdS4PZ-N0cuExeYL2IOu6C0aCqL6B-PjYe4,18629
3
+ ilang/ffi.py,sha256=0dM1Oo4OH-0r_dTbws7RomEJ1bMho9jKUXzPm0dYG6g,5023
4
+ ilang/inputs.py,sha256=pYQgclRFTgVGRleDOUQc_OCDnu-BUfcCX_sgNSbXg3s,2024
5
+ ilang/libi_core.dylib,sha256=X2K5ST24X22ZNmLZuJrkeScI_XBunIcSD7icPcpMaxM,1382848
6
+ ilang/tensor.py,sha256=3SsbDPQNW_ogES4Kz7khxOLKn2mVjrwrAyzD2y206T8,6347
7
+ ilang_python-0.1.0.dist-info/METADATA,sha256=osIqosptFQ5ufBCxGOOeG-gzowSI4x6VcCuoKKgPV_c,5059
8
+ ilang_python-0.1.0.dist-info/RECORD,,
9
+ ilang_python-0.1.0.dist-info/WHEEL,sha256=roArpf_MxwvZELSyjqDlac-lUD5_1viQfLUbIwp-24k,102
10
+ ilang_python-0.1.0.dist-info/licenses/LICENSE,sha256=q2NkHiIxxAaG46uZAoBv9ZQlc9OMZBpKv8djpcX5kmo,11341
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-macosx_11_0_arm64