From f9ec9f1527a2f5246131c488c9582f536c626a61 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 22 Jun 2026 23:17:09 +0900 Subject: [PATCH 1/3] feat: add to_buffer/from_buffer utils based on buffer protocol --- ggml/utils.py | 78 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/ggml/utils.py b/ggml/utils.py index 47b069b..79695e5 100644 --- a/ggml/utils.py +++ b/ggml/utils.py @@ -52,6 +52,27 @@ class GGML_TYPE(enum.IntEnum): GGML_TYPE_TO_NUMPY_DTYPE = {v: k for k, v in NUMPY_DTYPE_TO_GGML_TYPE.items()} +def to_buffer(tensor: ggml.ggml_tensor_p) -> memoryview: + """Get a writable memoryview of a ggml tensor's raw data (zero-copy). + + The returned buffer exposes the tensor's full byte span via the Python + buffer protocol. Its contents are only valid while the owning ggml context + is alive. + + Parameters: + tensor: ggml tensor + + Returns: + Writable memoryview backed by the tensor data + """ + data = ggml.ggml_get_data(tensor) + if data is None: + raise ValueError("tensor data is None") + nbytes = ggml.ggml_nbytes(tensor) + array = (ctypes.c_char * nbytes).from_address(data) + return memoryview(array) + + def to_numpy( tensor: ggml.ggml_tensor_p, shape: Optional[Tuple[int, ...]] = None, @@ -65,23 +86,13 @@ def to_numpy( Numpy array with a view of data from tensor """ ggml_type = GGML_TYPE(tensor.contents.type) - if ggml_type == GGML_TYPE.F16: - ctypes_type = ctypes.c_uint16 - else: - ctypes_type = np.ctypeslib.as_ctypes_type(GGML_TYPE_TO_NUMPY_DTYPE[ggml_type]) - - data = ggml.ggml_get_data(tensor) - if data is None: - raise ValueError("tensor data is None") - array = (ctypes_type * ggml.ggml_nelements(tensor)).from_address(data) + dtype = GGML_TYPE_TO_NUMPY_DTYPE[ggml_type] + array = np.frombuffer(to_buffer(tensor), dtype=dtype) n_dims = ggml.ggml_n_dims(tensor) shape_ = tuple(reversed(tensor.contents.ne[:n_dims])) strides = tuple(reversed(tensor.contents.nb[:n_dims])) - output = np.ctypeslib.as_array(array) - if ggml_type == GGML_TYPE.F16: - output.dtype = np.float16 # type: ignore return np.lib.stride_tricks.as_strided( - output, shape=shape if shape is not None else shape_, strides=strides + array, shape=shape if shape is not None else shape_, strides=strides ) @@ -111,6 +122,47 @@ def from_numpy(x: npt.NDArray[Any], ctx: ggml.ggml_context_p) -> ggml.ggml_tenso return tensor +def from_buffer( + buffer: Any, + ctx: ggml.ggml_context_p, + type: GGML_TYPE, + shape: Tuple[int, ...], +) -> ggml.ggml_tensor_p: + """Create a new ggml tensor with data copied from a buffer-protocol object. + + The buffer is assumed to be contiguous and laid out in row-major (C) order + with the given shape and ggml type. Unlike :func:`from_numpy`, this does not + require numpy and works with any object exposing the buffer protocol + (``bytes``, ``bytearray``, ``memoryview``, ``array.array``, etc.). + + Parameters: + buffer: source buffer-protocol object + ctx: ggml context + type: ggml type of the tensor + shape: shape of the tensor in row-major (C) order + + Returns: + New ggml tensor with data copied from buffer + """ + ne = tuple(reversed(shape)) + tensor = ggml.ggml_new_tensor( + ctx, + type.value, + len(ne), + (ctypes.c_int64 * len(ne))(*ne), + ) + src = memoryview(buffer).cast("B") + if ggml.ggml_get_data(tensor) is not None: + dst = to_buffer(tensor).cast("B") + if len(src) != len(dst): + raise ValueError( + f"buffer size ({len(src)} bytes) does not match tensor size " + f"({len(dst)} bytes)" + ) + dst[:] = src + return tensor + + def copy_to_cpu( ctx: ggml.ggml_context_p, tensor: ggml.ggml_tensor_p ) -> ggml.ggml_tensor_p: From 5344ee4d08ec0b27dab5e3092ce20cf0d54f6723 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 22 Jun 2026 23:25:18 +0900 Subject: [PATCH 2/3] test: add to_buffer/from_buffer test cases --- tests/test_utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 96a9f38..e31f200 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,5 @@ +import array + import ggml import ggml.utils @@ -20,6 +22,33 @@ def test_utils(): ggml.ggml_free(ctx) +def test_from_buffer_and_to_buffer(): + params = ggml.ggml_init_params(mem_size=16 * 1024 * 1024) + ctx = ggml.ggml_init(params) + assert ctx is not None + # build a tensor from a plain array.array (no numpy) + data = array.array("f", [1, 2, 3, 4, 5, 6]) + t = ggml.utils.from_buffer(data, ctx, ggml.utils.GGML_TYPE.F32, (2, 3)) + assert ggml.utils.get_shape(t) == (3, 2) + # to_buffer exposes a writable zero-copy view of the same bytes + buf = ggml.utils.to_buffer(t) + assert buf.nbytes == data.itemsize * len(data) + assert buf.readonly is False + assert bytes(buf) == data.tobytes() + ggml.ggml_free(ctx) + + +def test_from_buffer_size_mismatch(): + params = ggml.ggml_init_params(mem_size=16 * 1024 * 1024) + ctx = ggml.ggml_init(params) + assert ctx is not None + with pytest.raises(ValueError): + ggml.utils.from_buffer( + array.array("f", [1, 2, 3]), ctx, ggml.utils.GGML_TYPE.F32, (2, 3) + ) + ggml.ggml_free(ctx) + + def test_numpy_arrays(): params = ggml.ggml_init_params(mem_size=16 * 1024 * 1024) ctx = ggml.ggml_init(params) From 435d2a1c727167fb958d7f2e1f03152d2a607467 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 22 Jun 2026 23:28:45 +0900 Subject: [PATCH 3/3] docs: update changelog for buffer protocol support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d50cf5..fa6ca28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- feat: add buffer protocol support to utils by @aisk in #175 - docs: add JupyterLite browser playground by @abetlen in #173 ## [0.0.44]