Description
🚀 The feature, motivation and pitch
ExecuTorchTensor
is a lightweight Objective‑C wrapper for the ExecuTorch C++ Tensor
API. It provides a natural, efficient interface for creating, accessing, and manipulating tensor data without exposing the C++ complexity. With multiple initializers (from raw bytes, NSData
, scalar arrays, or single scalars), it allows users to create tensors in various ways. The wrapper also exposes key metadata - such as data type, shape, dimension order, strides, and element count - in Objective‑C types, which are bridged naturally to Swift. This enables seamless, high‑performance integration into iOS/macOS applications using ExecuTorch.
Alternatives
See the parent issue #8360 for motivation and alternatives overview.
Additional context
- The wrapper minimizes data copying. For instance, when constructing a tensor from raw bytes, the data is not duplicated.
- Metadata (shape, strides, dimension order) is cached as
NSArrays
for efficient repeated access. - Advanced functionality (e.g. extended convenience initializers or additional tensor manipulation methods) may be added in future iterations.
- The
nativeInstance
accessor exposes a pointer to the underlying C++ tensor (wrapped as aTensorPtr
) and is intended for internal use (e.g. when the tensor must be passed to other C++ APIs). This pointer is not exposed to Swift. - We also considered the design of the existing Swift interfaces for Tensor abstraction in various frameworks:
RFC (Optional)
The ExecuTorchTensor
wrapper provides a robust, multi-faceted Objective‑C interface to manage tensor objects backed by a native C++ TensorPtr
. It supports various initialization modes (from raw pointers, NSData
, arrays of scalars, or single scalar values), data access (both immutable and mutable), cloning, resizing, and creating patterned tensors (empty, full, ones, zeros, random, random normal, and random integer). Swift interoperability is achieved via NS_SWIFT_NAME
annotations, ensuring a natural Swift API.
Internal Representation
The tensor wrapper internally holds a C++ TensorPtr
:
::executorch::extension::TensorPtr _tensor;
Additional members include:
-
Cached metadata for
shape
,strides
, anddimensionOrder
. -
An optional
NSData
reference used when initializing from data. -
The underlying tensor pointer is exposed through a read‑only property (unavailable in Swift).
-
Properties
The following properties provide read‑only access to tensor metadata:
Objective-C
// The scalar type of the tensor.
@property(nonatomic, readonly) ExecuTorchDataType dataType;
// An array describing the dimensions.
@property(nonatomic, readonly) NSArray<NSNumber *> *shape;
// The order in which dimensions are stored.
@property(nonatomic, readonly) NSArray<NSNumber *> *dimensionOrder;
// An array of strides for each dimension.
@property(nonatomic, readonly) NSArray<NSNumber *> *strides;
// Indicates whether the shape is static or dynamic.
@property(nonatomic, readonly) ExecuTorchShapeDynamism shapeDynamism;
// The total number of elements in the tensor.
@property(nonatomic, readonly) NSInteger count;
// A pointer to the underlying C++ tensor.
@property(nonatomic, readonly) void *nativeInstance NS_SWIFT_UNAVAILABLE("");
Swift
print(tensor.dataType) // e.g. .float
print(tensor.shape) // e.g. [2, 3]
print(tensor.dimensionOrder) // e.g. [0, 1]
print(tensor.strides) // e.g. [3, 1]
print(tensor.shapeDynamism) // e.g. .dynamicBound
print(tensor.count) // e.g. 6
Key Methods:
- Initialization with a C++ Counterpart:
Creates a tensor wrapper from an existing native instance. This is used internally and is unavailable in Swift.
Objective-C
- (instancetype)initWithNativeInstance:(void *)nativeInstance NS_DESIGNATED_INITIALIZER NS_SWIFT_UNAVAILABLE("");
- Initialization with a Tensor:
Constructs a new tensor by sharing the data that another Tensor already uses, where the metadata like shape can be further modified, which allows creating a "view" on the Tensor's data without modifying the original Tensor.
Objective-C
- (instancetype)initWithTensor:(ExecuTorchTensor *)otherTensor NS_SWIFT_NAME(init(_:));
Swift
let tensor = Tensor(otherTensor)
- Copying
Tensor should conform to NSCopying
and implement corresponding methods:
- (instancetype)copy;
- (instancetype)copyWithZone:(nullable NSZone *)zone; // Impl only, not exposed to header.
Copy will use clone_tensor_ptr
internally to duplicate both the data and metadata that the Tensor owns and return a clone.
- Data Aceess
The API provides two methods to access the raw tensor data as immutable or mutable data:
Objective-C
- (void)bytesWithHandler:(void (^)(const void *pointer, NSInteger count, ExecuTorchDataType type))handler NS_SWIFT_NAME(bytes(_:));
- (void)mutableBytesWithHandler:(void (^)(void *pointer, NSInteger count, ExecuTorchDataType dataType))handler NS_SWIFT_NAME(mutableBytes(_:));
Swift
tensor.bytes { pointer, count, dataType in
print("Data type:", dataType)
print("Element count:", count)
}
tensor.mutableBytes { pointer, count, dataType in
guard dataType == .float else {
return
}
let floatBuffer = pointer.bindMemory(to: Float.self, capacity: count)
for i in 0..<count {
floatBuffer[i] = 0.0
}
}
- Resizing
The tensor can be resized by providing a new shape. If the underlying C++ resize function fails, an error is reported using the ExecuTorchErrorDomain
.
Objective-C
- (BOOL)resizeToShape:(NSArray<NSNumber *> *)shape error:(NSError **)error NS_SWIFT_NAME(resize(to:));
Swift:
try tensor.resize(to: [4, 4])
- Equality
Tensors are compared by checking the data type, element count, underlying raw data (via std::memcmp
), and metadata (shape, strides, dimension order, and shape dynamism).
Objective-C
- (BOOL)isEqualToTensor:(nullable ExecuTorchTensor *)other;
Swift:
let isEqual = tensor == anotherTensor
- Initialize with Bytes without Copying
These initializers wrap an external memory pointer without copying the data. Users can optionally supply strides, dimension order, and an explicit shape dynamism.
Objective-C
- (instancetype)initWithBytesNoCopy:(void *)pointer
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;
- (instancetype)initWithBytesNoCopy:(void *)pointer
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType;
- (instancetype)initWithBytesNoCopy:(void *)pointer
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;
- (instancetype)initWithBytesNoCopy:(void *)pointer
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType;
Swift:
let tensor = Tensor(bytesNoCopy: pointer, shape: [2, 3], dataType: .float)
- Initialize with Bytes
These methods copy data from a raw pointer into an internal buffer. The total byte count is computed from the provided shape and data type.
Objective-C
- (instancetype)initWithBytes:(const void *)pointer
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;
- (instancetype)initWithBytes:(const void *)pointer
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType;
- (instancetype)initWithBytes:(const void *)pointer
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;
- (instancetype)initWithBytes:(const void *)pointer
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType;
Swift
let tensor = Tensor(bytes: pointer, shape: [2, 3], strides: [3, 1], dimensionOrder: [0, 1], dataType: .float)
- Initialize with Data
Initialize a tensor directly from an NSData
instance. The initializer ensures the data buffer is large enough for the given shape and data type.
Objective-C
- (instancetype)initWithData:(NSData *)data
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;
- (instancetype)initWithData:(NSData *)data
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType;
- (instancetype)initWithData:(NSData *)data
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism;
- (instancetype)initWithData:(NSData *)data
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType;
Swift
let tensor = Tensor(data: data, shape: [2, 3], dataType: .float, shapeDynamism: .static)
- Initialize with an Array of Scalars
These initializers create a tensor from an array of scalar values (NSNumber
). The number of scalars must match the element count implied by the shape. Under the hood, each scalar is converted to its corresponding C++ type.
Objective-C
- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(init(_:shape:strides:dimensionOrder:dataType:shapeDynamism:));
- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
shape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dimensionOrder:(NSArray<NSNumber *> *)dimensionOrder
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(init(_:shape:strides:dimensionOrder:dataType:));
- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(init(_:shape:dataType:shapeDynamism:));
- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(init(_:shape:dataType:));
- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(init(_:dataType:shapeDynamism:));
- (instancetype)initWithScalars:(NSArray<NSNumber *> *)scalars NS_SWIFT_NAME(init(_:));
Swift
let tensorFromScalars = Tensor([1.0, 2.0, 3.0], shape: [3], dataType: .float, shapeDynamism: .dynamicBound)
- Initialize with a Scalar
Convenience initializers for single scalar values are provided with overloads for different primitive types. These wrap a single value as an array of one element, using an empty shape.
Objective-C
- (instancetype)initWithScalar:(NSNumber *)scalar
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(init(_:dataType:));
- (instancetype)initWithScalar:(NSNumber *)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithByte:(uint8_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithChar:(int8_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithShort:(int16_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithInt:(int32_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithLong:(int64_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithFloat:(float)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithDouble:(double)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithBool:(BOOL)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUInt16:(uint16_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUInt32:(uint32_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUInt64:(uint64_t)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithInteger:(NSInteger)scalar NS_SWIFT_NAME(init(_:));
- (instancetype)initWithUnsignedInteger:(NSUInteger)scalar NS_SWIFT_NAME(init(_:));
Swift
let pi = Tensor(3.14)
- Create an Empty Tensor
Return a tensor without any predefined values. Overloads allow creation based on a given shape or by mirroring an existing tensor’s metadata.
Objective-C
+ (instancetype)emptyTensorWithShape:(NSArray<NSNumber *> *)shape
strides:(NSArray<NSNumber *> *)strides
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(empty(shape:strides:dataType:shapeDynamism:));
+ (instancetype)emptyTensorWithShape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(empty(shape:dataType:shapeDynamism:));
+ (instancetype)emptyTensorWithShape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(empty(shape:dataType:));
+ (instancetype)emptyTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(empty(like:));
Swift
let emptyTensor = Tensor.empty(shape: [2, 3], dataType: .float)
- Create a Full Tensor
Create tensors filled with a given scalar value. There are overloads to either specify strides explicitly or to mimic an existing tensor’s layout.
Objective-C
+ (instancetype)fullTensorWithShape:(NSArray<NSNumber *> *)shape
scalar:(NSNumber *)scalar
strides:(NSArray<NSNumber *> *)strides
dataType:(ExecuTorchDataType)dataType
shapeDynamism:(ExecuTorchShapeDynamism)shapeDynamism NS_SWIFT_NAME(full(shape:scalar:strides:dataType:shapeDynamism:));
+ (instancetype)fullTensorWithShape:(NSArray<NSNumber *> *)shape
scalar:(NSNumber *)scalar
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(full(shape:scalar:dataType:));
+ (instancetype)fullTensorLikeTensor:(ExecuTorchTensor *)tensor
scalar:(NSNumber *)scalar NS_SWIFT_NAME(full(like:scalar:));
Swift
let fullTensor = Tensor.full(shape: [2, 3], scalar: 7, dataType: .int)
- Create a Tensor with Ones and Zeros
These are shortcuts for full tensors with a fill value of 1 or 0, respectively.
Objective-C
+ (instancetype)onesTensorWithShape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(ones(shape:dataType:));
+ (instancetype)onesTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(ones(like:));
+ (instancetype)zerosTensorWithShape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(zeros(shape:dataType:));
+ (instancetype)zerosTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(zeros(like:));
Swift
let onesTensor = Tensor.ones(shape: [3, 2], dataType: .float)
let zerosTensor = Tensor.zeros(shape: [2, 3], dataType: .double)
- Create Random Tensors
These methods create tensors with random values. Separate factory methods support uniform random values, normal distribution and random integers within a specified range.
Objective-C
+ (instancetype)randomTensorWithShape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(rand(shape:dataType:));
+ (instancetype)randomTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(rand(like:));
+ (instancetype)randomNormalTensorWithShape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(randn(shape:dataType:));
+ (instancetype)randomNormalTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(randn(like:));
+ (instancetype)randomIntegerTensorWithLow:(NSInteger)low
high:(NSInteger)high
shape:(NSArray<NSNumber *> *)shape
dataType:(ExecuTorchDataType)dataType NS_SWIFT_NAME(randint(low:high:shape:dataType:));
+ (instancetype)randomIntegerTensorLikeTensor:(ExecuTorchTensor *)tensor NS_SWIFT_NAME(randint(like:low:high:));
Swift
let randTensor = Tensor.rand(shape: [2, 3], dataType: .float)
let randnTensor = Tensor.randn(shape: [10], dataType: .int)
let randintTensor = Tensor.randint(low: 0, high: 10, shape: [2, 3], dataType: .int)
- Error Handling & Reporting:
Methods like resizeToShape:error:
return a boolean value indicating success. If an error occurs, an NSError
is populated using the domain ExecuTorchErrorDomain
. Accessor properties are read-only and rely on cached metadata; if such metadata is not available, they return nil or default values, following Objective‑C conventions. In Swift, these methods appear as throwing functions.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status