Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 86 additions & 60 deletions sycl/doc/extensions/experimental/sycl_ext_oneapi_matrix.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ value to determine which of the extension's APIs the implementation supports.
|======================
|Value |Description
|1 |Initial extension implementation on Intel AMX. Base features are supported.
|2 |Initial extension JIT implementation on Intel AMX and DPAS. load, store, mad and the query interface are supported
|2 |Initial extension JIT implementation on Intel AMX and DPAS. load, store, mad, fill, piece-wise operations, and the query interface are supported
|======================

## New `joint_matrix` class
Expand Down Expand Up @@ -165,6 +165,85 @@ namespace sycl::ext::oneapi::experimental::matrix {
The matrix multiply and add function performs the multiply operation on the matrices `A` and `B`, accumulate the result with `C` and return the result.


#### Matrix Initialization: `joint_matrix_fill`
The current interface presented above assumes that all the matrices are directly loaded from memory. This new function called `joint_matrix_fill` makes it possible to multiply a matrix which is not directly loaded from memory but rather initialized directly in the register. On Intel AMX, if the initialization constant is zero, this would map to `_tile_zero` intrinsic:

```c++
namespace sycl::ext::oneapi::experimental::matrix {
template <typename Group, typename T, size_t NumRows, size_t NumCols,
matrix_layout L, typename Tv>
void joint_matrix_fill(Group sg, joint_matrix<T, NumRows, NumCols, L, Group> &m, const Tv v);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void joint_matrix_fill(Group sg, joint_matrix<T, NumRows, NumCols, L, Group> &m, const Tv v);
void joint_matrix_fill(Group sg, joint_matrix<T, NumRows, NumCols, L, Group> &m, Tv v);

It does not make sense to use const here when passing a parameter by value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there separate types for T and Tv? Don't you want v to have the same type T?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v can have a different type

}
```
IMPORTANT: In the current implementation, only the subgroup scope is supported.

#### Element Indexing and Piece-Wise Operations
Besides matrix multiply and add, matrices are used in linear and non linear piece-wise operations. Activation functions are an example of element-wise operations. They can be linear like `ReLU` that, for each value `z`, returns the maximum between `z` and zero, or non linear like `Sigmoid` that calculates `1/(1+ exp(-z))`. Quantization that is needed for conversion between low precision types like `int8_t` and `fp32` uses piece-wise operations. For instance, quantized GEMM for `int8_t` is calculated using `A*B + sum_rows_A + sum_cols_B + scalar_zero_point`. `sum_rows_A` and `sum_cols_B` do not operate on elements of the matrix but on pieces: row in `sum_rows_A` and columns in `sum_cols_B`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the last sentence about quantized GEMM. How do the APIs in this section help compute the sum or rows or columns in A or B?

I think the main point here is that the element indexing API can be used in cases where some computation needs to be performed individually on each element of the matrix. Correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is that: element-wise operations where computation needs to be performed individually on each element of the matrix. But there is also piece-wise operations like sum of rows. An example on how to do that using get_wi_data can be:

auto data = C.get_wi_data();
for (int i = 0; i < length; ++i) {
auto row, col = data[i].get_coord();
sum_of_local_rows[row] += data[i];
}

But in this case, the user needs to know the mapping WI data to matrix first. As you can see here, the mapping function is missing. I added in the future looking API the mapping function as future work.

Copy link
Contributor

@JackAKirk JackAKirk May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all that needs to be stated is that:

Suggested change
Besides matrix multiply and add, matrices are used in linear and non linear piece-wise operations. Activation functions are an example of element-wise operations. They can be linear like `ReLU` that, for each value `z`, returns the maximum between `z` and zero, or non linear like `Sigmoid` that calculates `1/(1+ exp(-z))`. Quantization that is needed for conversion between low precision types like `int8_t` and `fp32` uses piece-wise operations. For instance, quantized GEMM for `int8_t` is calculated using `A*B + sum_rows_A + sum_cols_B + scalar_zero_point`. `sum_rows_A` and `sum_cols_B` do not operate on elements of the matrix but on pieces: row in `sum_rows_A` and columns in `sum_cols_B`.
Besides matrix multiply and add, this extension aims to make it possible to perform piece-wise operations on matrices in a SPMD manner. The mechanisms that are recommended to perform such piece-wise operations depend upon which of the following classes the operation falls into:
Class "1". Element-wise operations that are performed identically on every element of the matrix.
Class "2". Element-wise operations that depend on the element index of the matrix or operations that take multiple elements as operands (such as a sum of all elements in a row for example).
This extension currently only supports case 1). However a proposal for supporting 2) (for some backends) in the future is provided in a later section.

Then continue with the explanation of how case 1) is dealt with. Case 2) seems to have been considered in section "### WI data to joint matrix mapping coordinates information for piece-wise operations" and requires that the backend knows the mapping from "joint_matrix Domain" to "WI Domain".

Copy link
Contributor

@JackAKirk JackAKirk May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically case 1) doesn't require mapping between get_data and joint_matrix, but cases 2) do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make the change, thanks


We explored multiple options to enable this feature in the matrix interface: 1) Allowing non-restrictive element indexing on the matrix elements would result into slow indexing on the GPU, 2) Operator overloading can represent only element-wise operations and not the operations on pieces (row, column, diagonal, etc) of the matrix. 3) Providing specific functions for these piece-wise operations can resolve some of the functions we know of today like the ones involved in quantization but it is not general to any problem that may occur in the future.
Copy link
Contributor

@JackAKirk JackAKirk May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the paragraph beginning "We explored" can be removed also I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay


In order to be able to perform any piece-wise operation in a general and in an efficient way, we provide a mapping conversion function from the matrix domain that is owned by a group of work items to the portion that is owned by a work item. Besides, the WI data to joint matrix mapping coordinates information must be known.

Nvidia wmma interface added a new member to `fragment` class to designate the WI owned part of the matrix.
Copy link
Contributor

@JackAKirk JackAKirk May 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to remove this whole paragraph beginning "Nvidia wmma interface" because it (or something similar) is more appropriate for the CUDA backend spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

While this provides fast element indexing on the GPU compared to the non-restrictive option, the user does not know the mapping of the owned data to the original matrix.
However using the `mma` ptx instructions as opposed to the `wmma` ptx instructions the mapping is known. Knowing this mapping is important for the user to implement new operations like sum of rows of a matrix for quantized algorithms.

##### Solution: Explicit conversion with mapping from SIMD to SPMD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remaining paragraphs above seem inappropriate for an API specification. The audience for this document wants to know what this API does and how to use it. However, these paragraphs seem more like a justification for why this API was chosen vs. some other possibility. That's not really the purpose of this document. I'd suggest either removing them or moving them to a new section towards the bottom of the document titled something like "Background on the element indexing operations".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will move these in a background subsection but I will leave it in this section. Let's see if it looks better.

We introduce a new function `get_wi_data` that provides a view of the portion of the matrix that is owned by the current WI.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence should be expanded to explain the purpose of this API better. The reader needs to understand that each work-item contains only a subset of the elements in the matrix. The sentence above sort of mentions this, but I think it could be clearer. For example:

The data elements in a joint_matrix distributed across the work-items in the Group in an implementation-defined way, such that each work-item owns a unique subset of the data elements. An application can use the APIs in this section to access the data elements owned by each work-item. This is especially useful for algorithms that operate on each data element individually.

I think this last sentence could replace the first paragraph you have "Besides matrix multiply and add, matrices are used in linear ...". However, if you think there's more to say about when these APIs are useful, you could add some more sentences here explaining it.

Then finish up by saying something like:

The code listing below shows a synopsis of these new APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already added more clarifications based on Jack's first review. I will add more based on your input as well. However, note that , "such that each work-item owns a unique subset of the data elements" is not always true like in the AMX case for instance. A matrix is allocated in the 2d register tile that is a subgroup shared memory (register in this case).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, note that , "such that each work-item owns a unique subset of the data elements" is not always true like in the AMX case for instance. A matrix is allocated in the 2d register tile that is a subgroup shared memory (register in this case).

Are you saying that when one work-item calls get_wi_data that it might get overlapping elements that are also returned from some other work-item's call to get_wi_data? If this is the case, I don't see how this API is very useful. For example, code like this would result in some elements being incremented twice:

auto wi_data_c = matC.get_wi_data();
for (int i = 0; i < wi_data_c.length(); i++)
  wi_data_c[i] += 1;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying that when one work-item calls get_wi_data that it might get overlapping elements

No this is not possible


```c++
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of these code synopses is to show the API, not the implementation. Therefore, remove the function bodies and all the private data members. For example:

namespace sycl::ext::oneapi::experimental::matrix {

template <typename T, size_t NumRows, size_t NumCols,
          matrix_layout Layout = matrix_layout::row_major,
          typename Group = sycl::sub_group>
struct joint_matrix {
   wi_data<T, NumRows, NumCols, Layout, Group> get_wi_data();
};

/* ... */

} // namespace sycl::ext::oneapi::experimental::matrix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do that, thanks

namespace sycl::ext::oneapi::experimental::matrix {
template <typename T, size_t NumRows, size_t NumCols,
matrix_layout Layout = matrix_layout::row_major,
typename Group = sycl::sub_group>
struct joint_matrix {
joint_matrix(Group sg) {…}
wi_data<T, NumRows, NumCols, Layout, Group> get_wi_data() {
return wi_data<T, NumRows, NumCols, Layout, Group>(*this);
}
};
template <typename T, size_t NumRows, size_t NumCols, matrix_layout Layout, typename Group>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do wi_data and wi_element really need all these template parameters? It seems like it would be easier to use if the only template parameter was T. It seems like the other template parameters are only there because there is a private data member M (a reference to matrix). However, you only seem to use M.spvm in the function bodies. Could you instead just store the spvm member directly in wi_data and wi_element?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on your suggestion? I get what you want to do but did not get the how.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delayed response. I was OOO for about 2 months and just got back recently.

I was thinking that these types could be simplified to have fewer template parameters, so the goal would be to have an API like this:

template <typename T>
class wi_data {
  size_t length();
  wi_element<T> operator[](size_t i);
};

template <typename T>
class wi_element {
  operator T();
  wi_element &operator=(const T &rhs);
};

Very roughly, I was thinking that you could accomplish this by changing the private data member included in wi_data and wi_element. Currently, these both contain a reference to the joint matrix M. However, it seems like they only need to use M.spvm. Therefore, could you change the implementation to hold just the spvm like:

template <typename T>
class wi_data {
  /* not sure what type */ spvm;
public:
  size_t length() {return __spirv_JointMatrixWorkItemLengthINTEL(spvm);}
  wi_element<T> operator[](size_t i) {
    return wi_element<T>(spvm, i);
  }
};

template <typename T>
class wi_element {
  /* not sure what type */ spvm;
  std::size_t idx;

public:
  operator T() {
    return __spirv_VectorExtractDynamic(spvm, idx);
  }
  wi_element &operator=(const T &rhs) {
    M.spvm = __spirv_VectorInsertDynamic(spvm, rhs, idx);
    return *this;
  }
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gmlueck
It looks like spvm type also needs these template parameters. SO I don't think we can reduce them:
__spv::__spirv_JointMatrixINTEL<
T, NumRows, NumCols, spv_matrix_layout_traits::value> *spvm;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's too bad.

class wi_data {
joint_matrix<T, NumRows, NumCols, Layout, Group> &M;
public:
wi_data(joint_matrix<T, NumRows, NumCols, Layout, Group> &Mat) : M(Mat){}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume we do NOT want application code to construct a wi_data (or wi_element) directly? Instead, I presume we want application to call joint_matrix::get_wi_data to get the wi_data? If that is the case, these constructors should be private in the implementation, and joint_matrix should be a friend, so that it can construct the objects.

The code synopsis, then, would only list the public member functions:

template <typename T, size_t NumRows, size_t NumCols, matrix_layout Layout, typename Group>
class wi_data {
 public:
  size_t length();
  wi_element<T, NumRows, NumCols, Layout, Group> operator[](size_t i);
};

template <typename T, size_t NumRows, size_t NumCols, matrix_layout Layout, typename Group>
class wi_element {
 public:
  operator T();
  wi_element &operator=(const T &rhs);
};

size_t length() {return __spirv_JointMatrixWorkItemLengthINTEL(M.spvm);}
wi_element<T, NumRows, NumCols, Layout, Group> operator[](size_t i) {
return wi_element<T, NumRows, NumCols, Layout, Group>(M, i);
}
};
template <typename T, size_t NumRows, size_t NumCols,
matrix_layout Layout = matrix_layout::row_major,
typename Group = sycl::sub_group>
class wi_element {
joint_matrix<T, NumRows, NumCols, Layout, Group> &M;
std::size_t idx;

public:
wi_element(joint_matrix<T, NumRows, NumCols, Layout, Group> &Mat, std::size_t i): M(Mat), idx(i) {}
operator T() {
return __spirv_VectorExtractDynamic(M.spvm, idx);
}
wi_element &operator=(const T &rhs) {
M.spvm = __spirv_VectorInsertDynamic(M.spvm, rhs, idx);
return *this;
}
};
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the code synopsis, there should be some description of the member functions. I'd suggest three tables, one for each class:

  • Table describing member functions of joint_matrix (get_wi_data)
  • Table describing member functions of wi_data
  • Table describing member functions of wi_element.

You can see an example here: https://github.com/intel/llvm/blob/sycl/sycl/doc/extensions/proposed/sycl_ext_oneapi_device_global.asciidoc#representation-of-device-globals

(Scroll down to the table after the code synopsis.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we don't have that many members, I added description in the text. Let me know if it looks enough.


Example where each WI gets a vector in `wi_data_c`. Vectorization along the subgroup dimension will get enabled automatically to vectorize the contiguous portion of the matrix.
```c++
auto wi_data_c = matC.get_wi_data();
for (int i = 0; i < wi_data_c.length(); i++)
wi_data_c[i] *= alpha; // Note that the indexing here “i” is in the vector owned by a WI, not in the matrix C
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
wi_data_c[i] *= alpha; // Note that the indexing here “i” is in the vector owned by a WI, not in the matrix C
wi_data_c[i] *= alpha; // Note that the indexing here "i" is in the vector owned by a WI, not in the matrix C

```

IMPORTANT: In the current implementation, only the subgroup scope is supported.

IMPORTANT: The WI data to joint matrix mapping coordinates information is not implemented yet.

## VNNI/Packed Layout
Intel AMX and DPAS compute assumes register for B tile (src1) to be in VNNI format as they need 32bit of K-data in A and B to be contiguous in memory.
The VNNI blocking factor is 2 in the case of 16-bit types, and it is 4 in the case of 8-bit types. While the current implementation assumes that the matrix has been already packed by the user for performance reasons, the layout information is needed to inform the implementation about this transform. The following example illustrates how a matrix in `row_major` layout is transformed into the `packed_b` layout for a 16-bit type.
Expand Down Expand Up @@ -225,12 +304,15 @@ q.parallel_for(nd_range<2>(G, L), [=](nd_item<2> item)
// users need to specify the packed_b layout
joint_matrix<int8_t, tK, tN, packed_b> tB(sg);
joint_matrix<int32_t, tM, tN> tC(sg);
joint_matrix_load(sg, tC, memC + sg_startx * tM * N + sg_starty/SG_SIZE*tN, N, matrix_layout::row_major);
joint_matrix_fill(sg, tC, 0);
for (int k = 0; k < K; k += tk) {
joint_matrix_load(sg, tA, memA + sg_startx * tM * K + k, K, matrix_layout::row_major);
joint_matrix_load(sg, tB, memB + k * N + sg_starty/SG_SIZE*tN*4, N*4, matrix_layout::packed_b); // VNNI
tC = joint_matrix_mad(sg, tA, tB, tC);
}
auto wi_slice_c = matC.get_wi_data();
for (int i = 0; i < wi_slice_c.length(); i++)
wi_slice_c[i] *= alpha; // The indexing here “i” is in the vector owned by a WI, not in the matrix C
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
wi_slice_c[i] *= alpha; // The indexing here “i” is in the vector owned by a WI, not in the matrix C
wi_slice_c[i] *= alpha; // The indexing here "i" is in the vector owned by a WI, not in the matrix C

Minor nit: looks like you cut-and-paste this code from a Word document, which introduced non-ascii quote characters. They should be changed to standard double-quote characters.

joint_matrix_store(sg, tC, memC + sg_startx * tM * N + sg_starty/SG_SIZE*tN, N, matrix_layout::row_major);
}).wait();
```
Expand Down Expand Up @@ -509,63 +591,6 @@ joint_matrix<int, msize, nsize> sub_c(sg);

## Future-looking API

### Matrix Initialization: `joint_matrix_fill`
The current interface presented above assumes that all the matrices are directly loaded from memory. This new function called `joint_matrix_fill` makes it possible to multiply a matrix which is not directly loaded from memory but rather initialized directly in the register. On Intel AMX, if the initialization constant is zero, this would map to `_tile_zero` intrinsic:

```c++
namespace sycl::ext::oneapi::experimental::matrix {
template <typename Group, typename T, size_t NumRows, size_t NumCols,
matrix_layout L>
void joint_matrix_fill(Group sg, joint_matrix<T, NumRows, NumCols, L, Group> &m, const T& v);
}
```

### Element Indexing and Element-Wise Operations
There are multiple options on how to enable this feature.

#### Option 1: Non-restrictive element indexing
Allowing non-restrictive element indexing on the matrix element as shown below would result into slow indexing on the GPU.
Besides, it will rely heavily on spirv and compiler vectorization:

```c++
matrix<int, 8, 8> C;
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
C(i,j) *= alpha; //Align with mdspan
```
#### Option2: Restrictive fast element indexing
In the DPC++ context, the expectation is that all element-wise operations will happen in a converged control path by all work items in the group.
Option 2 proposes a new set of element-wise operations by overloading existing operations to work on `matrix` object. An example is shown below:
```c++
joint_matrix<ONEAPI::sub_group, int, 8, 8> C(sg);
C *= alpha;
```
The problem with this option is that it is restrictive to a very limited set of operations.

#### Option3: Restrictive conversion in the interface from SIMD to SPMD
Nvidia wmma interface added a new member to `fragment` class to designate the WI owned part of the matrix.
While this provides fast element indexing on the GPU compared to the non-restrictive option, the user does not know the mapping of the owned data to the original matrix.
However using the `mma` ptx instructions as opposed to the `wmma` ptx instructions the mapping is known. Knowing this mapping is important for the user to implement new operations like sum of rows of a matrix for quantized algorithms.

#### proposal: Explicit conversion in the interface from SIMD to SPMD
We introduce a new function `get_wi_data` that provides any portion of the matrix that the user wants but in a SPMD array object:.

```c++
namespace sycl::ext::oneapi::experimental::matrix {
template <typename Group, typename T, size_t NumRows, size_t NumCols, matrix_layout L>
marray<T, n_rows * n_cols> get_wi_data(joint_matrix<T, NumRows, NumCols, L, Group> &m, size_t row_index,
size_t col_index, size_t n_rows, size_t n_cols);
}
```

Example where each WI gets 1 column:
```c++
marray<T,msize> wi_C = get_wi_data(C, 0, wi_idx, msize, 1, matrix_layout::row_major);
for (int i = 0; i < msize; i++)
row_sum += wi_C[i];
```


### Memory scope
The current experimental API uses `joint_` semantics to define the memory scope of the matrix. The long term solution is to use the proposed link:../supported/sycl_ext_oneapi_local_memory.asciidoc[`group_local_memory` extension] to allocate the matrix in local memory associated with a SYCL group as shown in the example below.

Expand All @@ -585,7 +610,7 @@ We did not utilize this extension for this matrix API version because sub-group
- In the future looking APIs, `get_wi_data` (that is currently under design) returns an owned object. Should this return a view object to make sure the original matrix C is changed after its slices are modified.

## TODO List
- Add support for fill matrix and element-wise operations features
- Add WI data to joint matrix mapping coordinates information for piece-wise operations. This will be added as part of the query or new methods to the 'get_wi_data' class.
- Add 'matrix_use' parameter to the matrix to distinguish between matrix A, B, and matrix accumulator. This is necessary for supporting VNNI and transpose transform
- Change the names default sizes in the query from defaultM, defaultN, defaultK to M,N,K
- Change the type of `scope` in the query interface to be able to return more than one value. This will be useful in the event we support other scopes like workgroup besides subgroups
Expand All @@ -599,4 +624,5 @@ We did not utilize this extension for this matrix API version because sub-group
|Rev |Date |Author |Changes
|1 |2021-04-13 |Dounia Khaldi |Initial public working draft.
|2 |2021-10-05 |Dounia Khaldi |JIT implementation on both Intel AMX and DPAS
|3 |2022-05-16 |Dounia Khaldi |Add matrix fill and piece-wise operations support
|======================