Skip to content

Commit 8031da5

Browse files
authored
Implement torch.as_tensor, similar to numpy.asarray. (#7109)
* Implement torch.as_tensor, similar to numpy.asarray. torch.as_tensor behaves like torch.tensor except it avoids copies if possible; so also somewhat like tensor.new but without the size overloads. I didn't add a requires_grad field, because we haven't decided on the semantics such as as_param. * Remove requires_grad for doc.
1 parent 1f5b392 commit 8031da5

File tree

7 files changed

+112
-12
lines changed

7 files changed

+112
-12
lines changed

docs/source/tensors.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ A tensor can be constructed from a Python :class:`list` or sequence using the
4444
:meth:`~torch.Tensor.requires_grad_` or
4545
:meth:`~torch.Tensor.detach` to avoid a copy.
4646
If you have a numpy array and want to avoid a copy, use
47-
:func:`torch.from_numpy`.
47+
:func:`torch.as_tensor`.
4848

4949
An tensor of specific data type can be constructed by passing a
5050
:class:`torch.dtype` and/or a :class:`torch.device` to a

docs/source/torch.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Creation Ops
3333
range of distributions.
3434

3535
.. autofunction:: tensor
36+
.. autofunction:: as_tensor
3637
.. autofunction:: from_numpy
3738
.. autofunction:: zeros
3839
.. autofunction:: zeros_like

test/test_torch.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,45 @@ def test_new_tensor(self):
20432043
self.assertIs(torch.int, res1.dtype)
20442044
self.assertEqual(res1.get_device(), expected.get_device())
20452045

2046+
def test_as_tensor(self):
2047+
# from python data
2048+
x = [[0, 1], [2, 3]]
2049+
self.assertEqual(torch.tensor(x), torch.as_tensor(x))
2050+
self.assertEqual(torch.tensor(x, dtype=torch.float32), torch.as_tensor(x, dtype=torch.float32))
2051+
2052+
# from tensor (doesn't copy unless type is different)
2053+
y = torch.tensor(x)
2054+
self.assertIs(y, torch.as_tensor(y))
2055+
self.assertIsNot(y, torch.as_tensor(y, dtype=torch.float32))
2056+
if torch.cuda.is_available():
2057+
self.assertIsNot(y, torch.as_tensor(y, device='cuda'))
2058+
y_cuda = y.to('cuda')
2059+
self.assertIs(y_cuda, torch.as_tensor(y_cuda))
2060+
self.assertIs(y_cuda, torch.as_tensor(y_cuda, device='cuda'))
2061+
2062+
if TEST_NUMPY:
2063+
# doesn't copy
2064+
n = np.random.rand(5, 6)
2065+
n_astensor = torch.as_tensor(n)
2066+
self.assertEqual(torch.tensor(n), n_astensor)
2067+
n_astensor[0][0] = 250.7
2068+
self.assertEqual(torch.tensor(n), n_astensor)
2069+
2070+
# changing dtype causes copy
2071+
n = np.random.rand(5, 6).astype(np.float32)
2072+
n_astensor = torch.as_tensor(n, dtype=torch.float64)
2073+
self.assertEqual(torch.tensor(n, dtype=torch.float64), n_astensor)
2074+
n_astensor[0][1] = 250.8
2075+
self.assertNotEqual(torch.tensor(n, dtype=torch.float64), n_astensor)
2076+
2077+
# changing device causes copy
2078+
if torch.cuda.is_available():
2079+
n = np.random.randn(5, 6)
2080+
n_astensor = torch.as_tensor(n, device='cuda')
2081+
self.assertEqual(torch.tensor(n, device='cuda'), n_astensor)
2082+
n_astensor[0][2] = 250.9
2083+
self.assertNotEqual(torch.tensor(n, device='cuda'), n_astensor)
2084+
20462085
def test_diag(self):
20472086
x = torch.rand(100, 100)
20482087
res1 = torch.diag(x)

tools/autograd/templates/python_torch_functions.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ static PyObject * THPVariable_arange(PyObject* self, PyObject* args, PyObject* k
136136
END_HANDLE_TH_ERRORS
137137
}
138138

139+
static PyObject * THPVariable_as_tensor(PyObject* self, PyObject* args, PyObject* kwargs)
140+
{
141+
HANDLE_TH_ERRORS
142+
return THPVariable_Wrap(torch::utils::as_tensor(default_type(), args, kwargs));
143+
END_HANDLE_TH_ERRORS
144+
}
145+
139146
// The Python clamp() syntax has to be mapped to one of three C++ functions
140147
static PyObject * THPVariable_clamp(PyObject* module, PyObject* args, PyObject* kwargs)
141148
{
@@ -215,6 +222,7 @@ static PyObject * THPVariable_tensor(PyObject* self, PyObject* args, PyObject* k
215222

216223
static PyMethodDef torch_functions[] = {
217224
{"arange", (PyCFunction)THPVariable_arange, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
225+
{"as_tensor", (PyCFunction)THPVariable_as_tensor, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
218226
{"clamp", (PyCFunction)THPVariable_clamp, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
219227
{"dsmm", (PyCFunction)THPVariable_mm, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
220228
{"from_numpy", (PyCFunction)THPVariable_from_numpy, METH_STATIC | METH_O, NULL},

torch/_torch_docs.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ def parse_kwargs(desc):
4646
returned tensor. Default: ``False``.
4747
""")
4848

49+
factory_data_common_args = parse_kwargs("""
50+
data (array_like): Initial data for the tensor. Can be a list, tuple,
51+
NumPy ``ndarray``, scalar, and other types.
52+
dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor.
53+
Default: if None, infers data type from :attr:`data`.
54+
device (:class:`torch.device`, optional): the desired device of returned tensor.
55+
Default: if None, uses the current device for the default tensor type
56+
(see :func:`torch.set_default_tensor_type`). :attr:`device` will be the CPU
57+
for CPU tensor types and the current CUDA device for CUDA tensor types.
58+
requires_grad (bool, optional): If autograd should record operations on the
59+
returned tensor. Default: ``False``.
60+
""")
61+
4962
add_docstr(torch.abs,
5063
r"""
5164
abs(input, out=None) -> Tensor
@@ -388,6 +401,35 @@ def parse_kwargs(desc):
388401
[ 3., 6.]])
389402
""")
390403

404+
add_docstr(torch.as_tensor,
405+
r"""
406+
as_tensor(data, dtype=None, device=None) -> Tensor
407+
408+
Convert the data into a `torch.Tensor`. If the data is already a `Tensor` of the same `dtype` and `device`, no copy
409+
will be performed. Similarly, if the data is an ``ndarray`` of the corresponding `dtype` and the `device` is the cpu,
410+
no copy will be performed.
411+
412+
Args:
413+
{data}
414+
{dtype}
415+
{device}
416+
417+
Example::
418+
419+
>>> torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]])
420+
tensor([[ 0.1000, 1.2000],
421+
[ 2.2000, 3.1000],
422+
[ 4.9000, 5.2000]])
423+
424+
>>> a = numpy.array([1, 2, 3])
425+
>>> t = torch.from_numpy(a)
426+
>>> t
427+
tensor([ 1, 2, 3])
428+
>>> t[0] = -1
429+
>>> a
430+
array([-1, 2, 3])
431+
""".format(**factory_data_common_args))
432+
391433
add_docstr(torch.asin,
392434
r"""
393435
asin(input, out=None) -> Tensor
@@ -3514,16 +3556,10 @@ def parse_kwargs(desc):
35143556
:func:`torch.from_numpy`.
35153557
35163558
Args:
3517-
data (array_like): Initial data for the tensor. Can be a list, tuple,
3518-
NumPy ``ndarray``, scalar, and other types.
3519-
dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor.
3520-
Default: if None, infers data type from :attr:`data`.
3521-
device (:class:`torch.device`, optional): the desired device of returned tensor.
3522-
Default: if None, uses the current device for the default tensor type
3523-
(see :func:`torch.set_default_tensor_type`). :attr:`device` will be the CPU
3524-
for CPU tensor types and the current CUDA device for CUDA tensor types.
3525-
requires_grad (bool, optional): If autograd should record operations on the
3526-
returned tensor. Default: ``False``.
3559+
{data}
3560+
{dtype}
3561+
{device}
3562+
{requires_grad}
35273563
35283564
35293565
Example::
@@ -3546,7 +3582,7 @@ def parse_kwargs(desc):
35463582
35473583
>>> torch.tensor([]) # Create an empty tensor (of size (0,))
35483584
tensor([])
3549-
""")
3585+
""".format(**factory_data_common_args))
35503586

35513587
add_docstr(torch.range,
35523588
r"""

torch/csrc/utils/tensor_new.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,21 @@ Tensor tensor_ctor(const Type& type, PyObject* args, PyObject* kwargs) {
442442
throw std::runtime_error("tensor(): invalid arguments");
443443
}
444444

445+
Tensor as_tensor(const Type& type, PyObject* args, PyObject* kwargs) {
446+
// TODO: add requires_grad once we decide on semantics for sharing data.
447+
static PythonArgParser parser({
448+
"as_tensor(PyObject* data, *, ScalarType dtype=None, Device? device=None)",
449+
});
450+
451+
ParsedArgs<3> parsed_args;
452+
auto r = parser.parse(args, kwargs, parsed_args);
453+
if (r.idx == 0) {
454+
bool type_inference = r.isNone(1);
455+
return internal_new_from_data(
456+
typeWithDefault(r, 1, 2, type), r.deviceOptional(2), r.pyobject(0), false, false, type_inference);
457+
}
458+
throw std::runtime_error("tensor(): invalid arguments");
459+
}
445460

446461
Tensor new_tensor(const Type& type, PyObject* args, PyObject* kwargs) {
447462
static PythonArgParser parser({

torch/csrc/utils/tensor_new.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ at::Tensor legacy_tensor_new(const at::Type& type, PyObject* args, PyObject* kwa
1111
at::Tensor legacy_new_from_data(const at::Type& type, at::optional<Device> device, PyObject *data);
1212
at::Tensor sparse_coo_tensor_ctor(const at::Type& type, PyObject* args, PyObject* kwargs);
1313
at::Tensor tensor_ctor(const at::Type& type, PyObject* args, PyObject* kwargs);
14+
at::Tensor as_tensor(const at::Type& type, PyObject* args, PyObject* kwargs);
1415
at::Tensor new_tensor(const at::Type& type, PyObject* args, PyObject* kwargs);
1516
at::Tensor new_empty(const at::Type& type, PyObject* args, PyObject* kwargs);
1617
at::Tensor new_full(const at::Type& type, PyObject* args, PyObject* kwargs);

0 commit comments

Comments
 (0)