blob: 030209d9dac8c5f0f8623d0903076c297f68d359 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sysds.runtime.data;
import org.apache.commons.lang.NotImplementedException;
import org.apache.sysds.common.Types.BlockType;
import org.apache.sysds.common.Types.ValueType;
import org.apache.sysds.runtime.DMLRuntimeException;
import org.apache.sysds.runtime.controlprogram.caching.CacheBlock;
import org.apache.sysds.runtime.io.IOUtilFunctions;
import org.apache.sysds.runtime.matrix.data.MatrixBlock;
import org.apache.sysds.runtime.matrix.operators.BinaryOperator;
import org.apache.sysds.runtime.util.UtilFunctions;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
/**
* A <code>TensorBlock</code> is the most top level representation of a tensor. There are two types of data representation
* which can be used: Basic/Homogeneous and Data/Heterogeneous
* Basic supports only one <code>ValueType</code>, while Data supports multiple <code>ValueType</code>s along the column
* axis.
* The format determines if the <code>TensorBlock</code> uses a <code>BasicTensorBlock</code> or a <code>DataTensorBlock</code>
* for storing the data.
*/
public class TensorBlock implements CacheBlock, Externalizable {
private static final long serialVersionUID = -8768054067319422277L;
private enum SERIALIZED_TYPES {
EMPTY, BASIC, DATA
}
public static final int[] DEFAULT_DIMS = new int[]{0, 0};
public static final ValueType DEFAULT_VTYPE = ValueType.FP64;
private int[] _dims;
private boolean _basic = true;
private DataTensorBlock _dataTensor = null;
private BasicTensorBlock _basicTensor = null;
/**
* Create a <code>TensorBlock</code> with [0,0] dimension and homogeneous representation (aka. basic).
*/
public TensorBlock() {
this(DEFAULT_DIMS, true);
}
/**
* Create a <code>TensorBlock</code> with the given dimensions and the given data representation (basic/data).
* @param dims dimensions
* @param basic if true then basic <code>TensorBlock</code> else a data type of <code>TensorBlock</code>.
*/
public TensorBlock(int[] dims, boolean basic) {
_dims = dims;
_basic = basic;
}
/**
* Create a basic <code>TensorBlock</code> with the given <code>ValueType</code> and the given dimensions.
* @param vt value type
* @param dims dimensions
*/
public TensorBlock(ValueType vt, int[] dims) {
this(dims, true);
_basicTensor = new BasicTensorBlock(vt, dims, false);
}
/**
* Create a data <code>TensorBlock</code> with the given schema and the given dimensions.
* @param schema schema of the columns
* @param dims dimensions
*/
public TensorBlock(ValueType[] schema, int[] dims) {
this(dims, false);
_dataTensor = new DataTensorBlock(schema, dims);
}
/**
* Create a [1,1] basic FP64 <code>TensorBlock</code> containing the given value.
* @param value value to put inside
*/
public TensorBlock(double value) {
_dims = new int[]{1, 1};
_basicTensor = new BasicTensorBlock(value);
}
/**
* Wrap the given <code>BasicTensorBlock</code> inside a <code>TensorBlock</code>.
* @param basicTensor basic tensor block
*/
public TensorBlock(BasicTensorBlock basicTensor) {
this(basicTensor._dims, true);
_basicTensor = basicTensor;
}
/**
* Wrap the given <code>DataTensorBlock</code> inside a <code>TensorBlock</code>.
* @param dataTensor basic tensor block
*/
public TensorBlock(DataTensorBlock dataTensor) {
this(dataTensor._dims, false);
_dataTensor = dataTensor;
}
/**
* Copy constructor
* @param that <code>TensorBlock</code> to copy
*/
public TensorBlock(TensorBlock that) {
copy(that);
}
/**
* Reset all cells to 0.
*/
public void reset() {
if (_basic) {
if (_basicTensor == null)
_basicTensor = new BasicTensorBlock(DEFAULT_VTYPE, _dims, false);
_basicTensor.reset();
}
else {
if (_dataTensor == null)
_dataTensor = new DataTensorBlock(DEFAULT_VTYPE, _dims);
_dataTensor.reset();
}
}
/**
* Reset data with new dimensions.
* @param dims new dimensions
*/
public void reset(int[] dims) {
_dims = dims;
if (_basic) {
if (_basicTensor == null)
_basicTensor = new BasicTensorBlock(DEFAULT_VTYPE, _dims, false);
_basicTensor.reset(dims);
}
else {
if (_dataTensor == null)
_dataTensor = new DataTensorBlock(DEFAULT_VTYPE, _dims);
_dataTensor.reset(dims);
}
}
public boolean isBasic() {
return _basic;
}
public boolean isAllocated() {
if (_basic)
return _basicTensor != null && _basicTensor.isAllocated();
else
return _dataTensor != null && _dataTensor.isAllocated();
}
/**
* If data is not yet allocated, allocate.
* @return this <code>TensorBlock</code>
*/
public TensorBlock allocateBlock() {
if (_basic) {
if (_basicTensor == null)
_basicTensor = new BasicTensorBlock(DEFAULT_VTYPE, _dims, false);
_basicTensor.allocateBlock();
}
else {
if (_dataTensor == null)
_dataTensor = new DataTensorBlock(DEFAULT_VTYPE, _dims);
_dataTensor.allocateBlock();
}
return this;
}
public BasicTensorBlock getBasicTensor() {
return _basicTensor;
}
public DataTensorBlock getDataTensor() {
return _dataTensor;
}
/**
* Get the <code>ValueType</code> if this <code>TensorBlock</code> is homogeneous.
* @return <code>ValueType</code> if homogeneous, null otherwise
*/
public ValueType getValueType() {
if (_basic)
return _basicTensor == null ? DEFAULT_VTYPE : _basicTensor.getValueType();
else
return null;
}
/**
* Get the schema if this <code>TensorBlock</code> is heterogeneous.
* @return value type if heterogeneous, null otherwise
*/
public ValueType[] getSchema() {
if (_basic)
return null;
else {
if (_dataTensor == null) {
//TODO perf, do not fill, instead save schema
ValueType[] schema = new ValueType[getDim(1)];
Arrays.fill(schema, DEFAULT_VTYPE);
return schema;
}
else
return _dataTensor.getSchema();
}
}
public int getNumDims() {
return _dims.length;
}
@Override
public int getNumRows() {
return getDim(0);
}
@Override
public int getNumColumns() {
return getDim(1);
}
@Override
public long getInMemorySize() {
// TODO Auto-generated method stub
return 0;
}
@Override
public boolean isShallowSerialize() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isShallowSerialize(boolean inclConvert) {
// TODO Auto-generated method stub
return false;
}
@Override
public void toShallowSerializeBlock() {
// TODO Auto-generated method stub
}
@Override
public void compactEmptyBlock() {
// TODO Auto-generated method stub
}
@Override
public CacheBlock slice(int rl, int ru, int cl, int cu, CacheBlock block) {
if( !(block instanceof TensorBlock) )
throw new RuntimeException("TensorBlock.slice(int,int,int,int,CacheBlock) CacheBlock was no TensorBlock");
TensorBlock tb = (TensorBlock) block;
int[] dims = new int[_dims.length];
dims[0] = ru - rl + 1;
dims[1] = cu - cl + 1;
System.arraycopy(_dims, 2, dims, 2, _dims.length - 2);
tb.reset(dims);
int[] offsets = new int[dims.length];
offsets[0] = rl;
offsets[1] = cl;
return slice(offsets, tb);
}
@Override
public void merge(CacheBlock that, boolean appendOnly) {
// TODO Auto-generated method stub
}
public int getDim(int i) {
return _dims[i];
}
public int[] getDims() {
return _dims;
}
public long[] getLongDims() {
return Arrays.stream(_dims).mapToLong(i -> i).toArray();
}
/**
* Calculates the next index array. Note that if the given index array was the last element, the next index will
* be the first one.
*
* @param dims the dims array for which we have to decide the next index
* @param ix the index array which will be incremented to the next index array
*/
public static void getNextIndexes(int[] dims, int[] ix) {
int i = ix.length - 1;
ix[i]++;
//calculating next index
if (ix[i] == dims[i]) {
while (ix[i] == dims[i]) {
ix[i] = 0;
i--;
if (i < 0) {
//we are finished
break;
}
ix[i]++;
}
}
}
/**
* Calculates the next index array. Note that if the given index array was the last element, the next index will
* be the first one.
*
* @param ix the index array which will be incremented to the next index array
*/
public void getNextIndexes(int[] ix) {
getNextIndexes(_dims, ix);
}
public boolean isVector() {
return getNumDims() <= 2
&& (getDim(0) == 1 || getDim(1) == 1);
}
public boolean isMatrix() {
return getNumDims() == 2
&& (getDim(0) > 1 && getDim(1) > 1);
}
public long getLength() {
return UtilFunctions.prod(_dims);
}
public boolean isEmpty() {
return isEmpty(false);
}
public boolean isEmpty(boolean safe) {
if (_basic)
return _basicTensor == null || _basicTensor.isEmpty(safe);
else
return _dataTensor == null || _dataTensor.isEmpty(safe);
}
public long getNonZeros() {
if (!isAllocated())
return 0;
if (_basic)
return _basicTensor.getNonZeros();
else
return _dataTensor.getNonZeros();
}
public Object get(int[] ix) {
if (_basic && _basicTensor != null)
return _basicTensor.get(ix);
else if (_dataTensor != null)
return _dataTensor.get(ix);
return 0.0;
}
public double get(int r, int c) {
if (_basic && _basicTensor != null)
return _basicTensor.get(r, c);
else if (_dataTensor != null)
return _dataTensor.get(r, c);
return 0.0;
}
public void set(Object v) {
if (_basic) {
if (_basicTensor == null)
_basicTensor = new BasicTensorBlock(DEFAULT_VTYPE, _dims, false);
_basicTensor.set(v);
}
else {
if (_dataTensor == null)
_dataTensor = new DataTensorBlock(getSchema(), _dims);
_dataTensor.set(v);
}
}
public void set(MatrixBlock other) {
if (_basic)
_basicTensor.set(other);
else
throw new DMLRuntimeException("TensorBlock.set(MatrixBlock) is not yet implemented for heterogeneous tensors");
}
/**
* Set a cell to the value given as an `Object`.
* @param ix indexes in each dimension, starting with 0
* @param v value to set
*/
public void set(int[] ix, Object v) {
if (_basic) {
if (_basicTensor == null)
_basicTensor = new BasicTensorBlock(DEFAULT_VTYPE, _dims, false);
_basicTensor.set(ix, v);
}
else {
if (_dataTensor == null)
_dataTensor = new DataTensorBlock(getSchema(), _dims);
_dataTensor.set(ix, v);
}
}
/**
* Set a cell in a 2-dimensional tensor.
* @param r row of the cell
* @param c column of the cell
* @param v value to set
*/
public void set(int r, int c, double v) {
if (_basic) {
if (_basicTensor == null)
_basicTensor = new BasicTensorBlock(DEFAULT_VTYPE, _dims, false);
_basicTensor.set(r, c, v);
}
else {
if (_dataTensor == null)
_dataTensor = new DataTensorBlock(getSchema(), _dims);
_dataTensor.set(r, c, v);
}
}
/**
* Slice the current block and write into the outBlock. The offsets determines where the slice starts,
* the length of the blocks is given by the outBlock dimensions.
*
* @param offsets offsets where the slice starts
* @param outBlock sliced result block
* @return the sliced result block
*/
public TensorBlock slice(int[] offsets, TensorBlock outBlock) {
// TODO perf
int[] srcIx = offsets.clone();
int[] destIx = new int[offsets.length];
for (int l = 0; l < outBlock.getLength(); l++) {
outBlock.set(destIx, get(srcIx));
int i = outBlock.getNumDims() - 1;
destIx[i]++;
srcIx[i]++;
//calculating next index
while (destIx[i] == outBlock.getDim(i)) {
destIx[i] = 0;
srcIx[i] = offsets[i];
i--;
if (i < 0) {
//we are finished
return outBlock;
}
destIx[i]++;
srcIx[i]++;
}
}
return outBlock;
}
public TensorBlock copy(TensorBlock src) {
_dims = src._dims.clone();
_basic = src._basic;
if (_basic) {
_dataTensor = null;
_basicTensor = src._basicTensor == null ? null : new BasicTensorBlock(src._basicTensor);
}
else {
_basicTensor = null;
_dataTensor = src._dataTensor == null ? null : new DataTensorBlock(src._dataTensor);
}
return this;
}
/**
* Copy a part of another <code>TensorBlock</code>
* @param lower lower index of elements to copy (inclusive)
* @param upper upper index of elements to copy (exclusive)
* @param src source <code>TensorBlock</code>
* @return the shallow copy of the src <code>TensorBlock</code>
*/
public TensorBlock copy(int[] lower, int[] upper, TensorBlock src) {
if (_basic) {
if (src._basic) {
_basicTensor.copy(lower, upper, src._basicTensor);
}
else {
throw new DMLRuntimeException("Copying `DataTensor` into `BasicTensor` is not a safe operation.");
}
}
else {
if (src._basic) {
// TODO perf
_dataTensor.copy(lower, upper, new DataTensorBlock(src._basicTensor));
}
else {
_dataTensor.copy(lower, upper, src._dataTensor);
}
}
return this;
}
/**
* Copy a part of another <code>TensorBlock</code>. The difference to <code>copy()</code> is that
* this allows for exact sub-blocks instead of taking all consecutive data elements from lower to upper.
* @param lower lower index of elements to copy (inclusive)
* @param upper upper index of elements to copy (exclusive)
* @param src source <code>TensorBlock</code>
* @return the deep copy of the src <code>TensorBlock</code>
*/
public TensorBlock copyExact(int[] lower, int[] upper, TensorBlock src) {
int[] destIx = lower.clone();
int[] srcIx = new int[lower.length];
long length = src.getLength();
for (long l = 0; l < length; l++) {
set(destIx, src.get(srcIx));
int i = src.getNumDims() - 1;
srcIx[i]++;
destIx[i]++;
//calculating next index
while (srcIx[i] == src.getDim(i)) {
srcIx[i] = 0;
destIx[i] = lower[i];
i--;
if (i < 0) {
//we are finished
return this;
}
srcIx[i]++;
destIx[i]++;
}
}
return this;
}
// `getExactSerializedSize()`, `write(DataOutput)` and `readFields(DataInput)` have to match in their serialized
// form definition
@Override
public long getExactSerializedSize() {
// header size (_basic, _dims.length + _dims[*], type)
long size = 1 + 4 * (1 + _dims.length) + 1;
if (isAllocated()) {
if (_basic) {
size += 1 + getExactBlockDataSerializedSize(_basicTensor);
}
else {
size += _dataTensor._schema.length;
for (BasicTensorBlock bt : _dataTensor._colsdata) {
if (bt != null)
size += getExactBlockDataSerializedSize(bt);
}
}
}
return size;
}
/**
* Get the exact serialized size of a <code>BasicTensorBlock</code> if written by
* <code>TensorBlock.writeBlockData(DataOutput,BasicTensorBlock)</code>.
* @param bt <code>BasicTensorBlock</code>
* @return the size of the block data in serialized form
*/
public long getExactBlockDataSerializedSize(BasicTensorBlock bt) {
// nnz, BlockType
long size = 8 + 1;
if (!bt.isSparse()) {
switch (bt._vt) {
case UINT8:
size += 1 * getLength(); break;
case INT32:
case FP32:
size += 4 * getLength(); break;
case INT64:
case FP64:
size += 8 * getLength(); break;
case BOOLEAN:
//TODO perf bits instead of bytes
size += getLength(); break;
//size += Math.ceil((double)getLength() / 64); break;
case STRING:
int[] ix = new int[bt._dims.length];
for (int i = 0; i < bt.getLength(); i++) {
String s = (String) bt.get(ix);
size += IOUtilFunctions.getUTFSize(s == null ? "" : s);
getNextIndexes(bt.getDims(), ix);
}
break;
case UNKNOWN:
throw new NotImplementedException();
}
}
else {
throw new NotImplementedException();
}
return size;
}
@Override
public void write(DataOutput out) throws IOException {
//step 1: write header (_basic, dims length, dims)
out.writeBoolean(_basic);
out.writeInt(_dims.length);
for (int dim : _dims)
out.writeInt(dim);
//step 2: write block type
//step 3: if tensor allocated write its data
if (!isAllocated())
out.writeByte(SERIALIZED_TYPES.EMPTY.ordinal());
else if (_basic) {
out.writeByte(SERIALIZED_TYPES.BASIC.ordinal());
out.writeByte(_basicTensor.getValueType().ordinal());
writeBlockData(out, _basicTensor);
}
else {
out.writeByte(SERIALIZED_TYPES.DATA.ordinal());
//write schema and colIndexes
for (int i = 0; i < getDim(1); i++)
out.writeByte(_dataTensor._schema[i].ordinal());
for (BasicTensorBlock bt : _dataTensor._colsdata) {
//present flag
if (bt != null)
writeBlockData(out, bt);
}
}
}
/**
* Write a <code>BasicTensorBlock</code>.
* @param out output stream
* @param bt source <code>BasicTensorBlock</code>
* @throws IOException if writing with the output stream fails
*/
public void writeBlockData(DataOutput out, BasicTensorBlock bt) throws IOException {
out.writeLong(bt.getNonZeros()); // nnz
if (bt.isEmpty(false)) {
//empty blocks do not need to materialize row information
out.writeByte(BlockType.EMPTY_BLOCK.ordinal());
}
else if (!bt.isSparse()) {
out.writeByte(BlockType.DENSE_BLOCK.ordinal());
DenseBlock a = bt.getDenseBlock();
int odims = (int) UtilFunctions.prod(bt._dims, 1);
int[] ix = new int[bt._dims.length];
for (int i = 0; i < bt._dims[0]; i++) {
ix[0] = i;
for (int j = 0; j < odims; j++) {
ix[ix.length - 1] = j;
switch (bt._vt) {
case FP32: out.writeFloat((float) a.get(i, j)); break;
case FP64: out.writeDouble(a.get(i, j)); break;
case INT32: out.writeInt((int) a.getLong(ix)); break;
case INT64: out.writeLong(a.getLong(ix)); break;
case BOOLEAN: out.writeBoolean(a.get(i, j) != 0); break;
case STRING:
String s = a.getString(ix);
out.writeUTF(s == null ? "" : s);
break;
default:
throw new DMLRuntimeException("Unsupported value type: "+bt._vt);
}
}
}
}
else {
throw new NotImplementedException();
}
}
@Override
public void readFields(DataInput in) throws IOException {
//step 1: read header (_basic, dims length, dims)
_basic = in.readBoolean();
_dims = new int[in.readInt()];
for (int i = 0; i < _dims.length; i++)
_dims[i] = in.readInt();
//step 2: read block type
//step 3: if tensor allocated read its data
switch (SERIALIZED_TYPES.values()[in.readByte()]) {
case EMPTY:
break;
case BASIC:
_basicTensor = new BasicTensorBlock(ValueType.values()[in.readByte()], _dims, false);
readBlockData(in, _basicTensor);
break;
case DATA:
//read schema and colIndexes
ValueType[] schema = new ValueType[getDim(1)];
for (int i = 0; i < getDim(1); i++)
schema[i] = ValueType.values()[in.readByte()];
_dataTensor = new DataTensorBlock(schema, _dims);
for (int i = 0; i < _dataTensor._colsdata.length; i++) {
//present flag
if (_dataTensor._colsdata[i] != null)
readBlockData(in, _dataTensor._colsdata[i]);
}
break;
}
}
/**
* Read a <code>BasicTensorBlock</code>.
* @param in input stream
* @param bt destination <code>BasicTensorBlock</code>
* @throws IOException if reading with the input stream fails
*/
protected void readBlockData(DataInput in, BasicTensorBlock bt) throws IOException {
bt._nnz = in.readLong();
switch (BlockType.values()[in.readByte()]) {
case EMPTY_BLOCK:
reset(bt._dims);
return;
case DENSE_BLOCK: {
bt.allocateDenseBlock(false);
DenseBlock a = bt.getDenseBlock();
int odims = (int) UtilFunctions.prod(bt._dims, 1);
int[] ix = new int[bt._dims.length];
for (int i = 0; i < bt._dims[0]; i++) {
ix[0] = i;
for (int j = 0; j < odims; j++) {
ix[ix.length - 1] = j;
switch (bt._vt) {
case FP32: a.set(i, j, in.readFloat()); break;
case FP64: a.set(i, j, in.readDouble()); break;
case INT32: a.set(ix, in.readInt()); break;
case INT64: a.set(ix, in.readLong()); break;
case BOOLEAN: a.set(i, j, in.readByte()); break;
case STRING:
// FIXME readUTF is not supported for CacheDataInput
a.set(ix, in.readUTF());
break;
default:
throw new DMLRuntimeException("Unsupported value type: "+bt._vt);
}
}
}
break;
}
case SPARSE_BLOCK:
case ULTRA_SPARSE_BLOCK:
throw new NotImplementedException();
}
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
write(out);
}
@Override
public void readExternal(ObjectInput in) throws IOException {
readFields(in);
}
public TensorBlock binaryOperations(BinaryOperator op, TensorBlock thatValue, TensorBlock result) {
if( !LibTensorBincell.isValidDimensionsBinary(this, thatValue) )
throw new RuntimeException("Block sizes are not matched for binary cell operations");
if (!_basic || !thatValue.isBasic())
throw new RuntimeException("Binary operations on tensors only supported for BasicTensors at the moment");
//prepare result matrix block
ValueType vt = TensorBlock.resultValueType(getValueType(), thatValue.getValueType());
if (result == null || result.getValueType() != vt)
result = new TensorBlock(vt, _dims);
else {
result.reset(_dims);
}
LibTensorBincell.bincellOp(this, thatValue, result, op);
return result;
}
public static ValueType resultValueType(ValueType in1, ValueType in2) {
// TODO reconsider with operation types
if (in1 == ValueType.UNKNOWN || in2 == ValueType.UNKNOWN)
throw new DMLRuntimeException("Operations on unknown value types not possible");
else if (in1 == ValueType.STRING || in2 == ValueType.STRING)
return ValueType.STRING;
else if (in1 == ValueType.FP64 || in2 == ValueType.FP64)
return ValueType.FP64;
else if (in1 == ValueType.FP32 || in2 == ValueType.FP32)
return ValueType.FP32;
else if (in1 == ValueType.INT64 || in2 == ValueType.INT64)
return ValueType.INT64;
else if (in1 == ValueType.INT32 || in2 == ValueType.INT32)
return ValueType.INT32;
else // Boolean - Boolean
return ValueType.INT64;
}
}