Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an API to keep data on GPU: dataToGPU #5953

Merged
merged 28 commits into from
Jan 6, 2022
Merged

Conversation

lina128
Copy link
Collaborator

@lina128 lina128 commented Dec 17, 2021

This PR adds a convenient API to read data out as a texture, so that it can directly be rendered on canvas or used by downstream shaders.

To see the logs from the Cloud Build CI, please join either our discussion or announcement mailing list.


This change is Reviewable

@danwexler
Copy link

Very excited to see this new function. Will this be in v13?

@lina128 lina128 marked this pull request as ready for review December 19, 2021 00:06
@lina128 lina128 requested a review from pyu10055 December 19, 2021 00:06
Copy link
Collaborator

@pyu10055 pyu10055 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @lina128)


tfjs-backend-webgl/src/backend_webgl.ts, line 809 at r3 (raw file):

          size <= texSize,
          () => 'customTexShape is too small. ' +
              'Row by column by 4 should be equal or larger than the ' +

Row * Column * 4

Code quote:

Row by column by 4

tfjs-backend-webgl/src/backend_webgl_test.ts, line 752 at r3 (raw file):

});

// TODO(lina128): Debug issue in CI. Code runs fine in Safari WebGL1 in local.

is this failing due to texture is float16? Since the difference is only the ENVS, we delete this section when we figure out CI issue for WebGL1.


tfjs-core/src/tensor.ts, line 369 at r3 (raw file):

  /**
   * Synchronously copy the tensor's data to a new GPU resource. Comparing to

This is async copy in terms of JS, since it is runs a webGL program, the JS return does not guarantee completion of shader execution.
But for WebGL this is sync, since the GPU queue is synchronized.


tfjs-backend-webgl/src/gpgpu_util.ts, line 227 at r2 (raw file):

              gl.TEXTURE_2D, 0, 0, 0, pixels.width, pixels.height, gl.RGBA,
              gl.UNSIGNED_BYTE, (pixels as PixelData).data));
      gl.flush();

gl.flush() I think this line should be removed, I guess I added by accident, this should not be needed.

Copy link
Collaborator Author

@lina128 lina128 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @pyu10055)


tfjs-backend-webgl/src/backend_webgl.ts, line 809 at r3 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

Row * Column * 4

Done.


tfjs-backend-webgl/src/backend_webgl_test.ts, line 752 at r3 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

is this failing due to texture is float16? Since the difference is only the ENVS, we delete this section when we figure out CI issue for WebGL1.

Fixed.


tfjs-core/src/tensor.ts, line 369 at r3 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

This is async copy in terms of JS, since it is runs a webGL program, the JS return does not guarantee completion of shader execution.
But for WebGL this is sync, since the GPU queue is synchronized.

Removed the confusing wording.


tfjs-backend-webgl/src/gpgpu_util.ts, line 227 at r2 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

gl.flush() I think this line should be removed, I guess I added by accident, this should not be needed.

Will fix in a separate PR.

@lina128 lina128 requested a review from pyu10055 December 20, 2021 21:13
@lina128
Copy link
Collaborator Author

lina128 commented Dec 20, 2021

Very excited to see this new function. Will this be in v13?

@danwexler , yes.

Copy link
Contributor

@qjia7 qjia7 left a comment

Choose a reason for hiding this comment

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

I don't notice that you pass a canvas context to webgl backend. It seems that the returned GPUResource is still limited in current tfjs context. How should users use this texture to another webgl program?

return gpuResouorce;
}

if (values != null && texture == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If values is not null, can we just upload the data to a texture and return the texture?

Copy link
Collaborator Author

@lina128 lina128 left a comment

Choose a reason for hiding this comment

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

Hi Jiajia, yes, GPUResource is still limited in current tfjs context. We suggest them to pass the canvas to use when creating webgl backend. Here's the other PR that allows user to pass a canvas: #5983

Reviewable status: 0 of 1 approvals obtained (waiting on @pyu10055 and @qjia7)


tfjs-backend-webgl/src/backend_webgl.ts, line 415 at r5 (raw file):

Previously, qjia7 (Jiajia Qin) wrote…

If values is not null, can we just upload the data to a texture and return the texture?

I have thought about this, it's hard to say which way is more convenient to users, so we'll see. Normally, if values are not null, it's either a small amount of data or the values haven't been uploaded yet, in both cases, users may have their preferred way of using the data, not necessarily using TFJS formatted data on GPU, so I prefer to leave it to user for now. However, if there's such request to provide TFJS formatted texture in the future, we can definitely support it.

Copy link
Collaborator

@pyu10055 pyu10055 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @lina128, @pyu10055, and @qjia7)


tfjs-backend-webgl/src/backend_webgl.ts, line 415 at r5 (raw file):

Previously, lina128 (Na Li) wrote…

I have thought about this, it's hard to say which way is more convenient to users, so we'll see. Normally, if values are not null, it's either a small amount of data or the values haven't been uploaded yet, in both cases, users may have their preferred way of using the data, not necessarily using TFJS formatted data on GPU, so I prefer to leave it to user for now. However, if there's such request to provide TFJS formatted texture in the future, we can definitely support it.

might be good to throw error instead of returning null. given that we don't support reading on CPU data to GPU.


tfjs-backend-webgl/src/backend_webgl.ts, line 423 at r5 (raw file):

    // Make engine track this tensor, so that we can dispose it later.
    engine().makeTensorFromDataId(

how user can track this tensor and dispose later?


tfjs-backend-webgl/src/backend_webgl_test.ts, line 682 at r5 (raw file):

});

describeWithFlags('keeping data on gpu ', WEBGL2_ENVS, () => {

can you add tests for memory leak check?


tfjs-core/src/tensor.ts, line 392 at r5 (raw file):

   * @doc {heading: 'Tensors', subheading: 'Classes'}
   */
  dataToGPU(options?: DataToGPUOptions): GPUResource {

should the GPUResource also contains reference to the tensor? so call can dispose it later?

Copy link
Collaborator Author

@lina128 lina128 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @lina128, @pyu10055, and @qjia7)


tfjs-backend-webgl/src/backend_webgl.ts, line 423 at r5 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

how user can track this tensor and dispose later?

The GPUResource has dataId, which is the only thing needed when disposing tensor. User can dispose it the same way as tensor, for typescript user, they may need to cast GPUResource to tensor first. So something like: tf.dispose(gpuResource as {} as Tensor).


tfjs-core/src/tensor.ts, line 392 at r5 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

should the GPUResource also contains reference to the tensor? so call can dispose it later?

It has reference to the tensor, dataId.

Copy link
Collaborator

@pyu10055 pyu10055 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @lina128, @pyu10055, and @qjia7)


tfjs-core/src/tensor.ts, line 392 at r5 (raw file):

Previously, lina128 (Na Li) wrote…

It has reference to the tensor, dataId.

can tidy automatically dispose GPUResource?

Copy link
Collaborator Author

@lina128 lina128 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @pyu10055 and @qjia7)


tfjs-backend-webgl/src/backend_webgl.ts, line 415 at r5 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

might be good to throw error instead of returning null. given that we don't support reading on CPU data to GPU.

Done.


tfjs-backend-webgl/src/backend_webgl_test.ts, line 682 at r5 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

can you add tests for memory leak check?

Done.


tfjs-core/src/tensor.ts, line 392 at r5 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

can tidy automatically dispose GPUResource?

It should behave the same as Tensor, added a test to verify.

*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
dataToGPU(options?: DataToGPUOptions): GPUResource {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, why support customTexShape? Is there any special requirement for this?

Copy link
Collaborator Author

@lina128 lina128 left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 1 approvals obtained (waiting on @pyu10055 and @qjia7)


tfjs-backend-webgl/src/backend_webgl.ts, line 423 at r5 (raw file):

Previously, lina128 (Na Li) wrote…

The GPUResource has dataId, which is the only thing needed when disposing tensor. User can dispose it the same way as tensor, for typescript user, they may need to cast GPUResource to tensor first. So something like: tf.dispose(gpuResource as {} as Tensor).

Hi Ping, you are right, it's better to track this tensor instead of just use dataId, I added the memory test. Previously, the tensor cannot be disposed properly because GPUResource is not recognized as instanceOf Tensor in one place in engine. After the latest change, it should work. Also see the memory test.


tfjs-core/src/tensor.ts, line 392 at r6 (raw file):

Previously, qjia7 (Jiajia Qin) wrote…

Just curious, why support customTexShape? Is there any special requirement for this?

The main use case is if user set the texShape to be the same as the canvas shape, then they can directly render the result on canvas. If they don't specify the texShape, we try to make a squarish texture.

Copy link
Collaborator

@pyu10055 pyu10055 left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 1 of 1 approvals obtained (waiting on @lina128, @pyu10055, and @qjia7)


tfjs-backend-webgl/src/backend_webgl_test.ts, line 802 at r7 (raw file):

      const b = tf.add(a, 0);
      b.dataToGPU();
      return b

missing ;

Copy link
Collaborator Author

@lina128 lina128 left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: complete! 1 of 1 approvals obtained (waiting on @pyu10055 and @qjia7)


tfjs-backend-webgl/src/backend_webgl_test.ts, line 802 at r7 (raw file):

Previously, pyu10055 (Ping Yu) wrote…

missing ;

Done.

@xhcao
Copy link
Contributor

xhcao commented Apr 19, 2022

Hi, @lina128 , you had replied Jiajia's as below,
"The main use case is if user set the texShape to be the same as the canvas shape, then they can directly render the result on canvas. If they don't specify the texShape, we try to make a squarish texture."

I have a confusion here, If canvas size is 44, and texture size is 22, we could directly render this texture to the canvas, and hardware will use texture LINEAR or NEAREST filter sampling to render the content.
And if texture size 2*2, and its content is [1, 2, 3, 4], and call dataToGPU(..., [4,4]) to create a new texture, whose content is [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] (Assume it is correct), these content is expected data for users? Are there rules for result when enlarging the texture size.

@lina128
Copy link
Collaborator Author

lina128 commented Apr 21, 2022

Hi @xhcao , I think for WebGL, if the texture is larger than the data size, the unused space will be 0. We try to store data in dense format, so the content will be [1, 2, 3, 4, 0, ..., 0].

@xhcao
Copy link
Contributor

xhcao commented Apr 22, 2022

Hi, @lina128 , I had tested unit case on webgl backend,
it('uses user defined texShape [3, 3].', () => {
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const a = tf.tensor(data, [1, 3, 4]);
const b = tf.add(a, 0);
const texShape = [3, 3] as [number, number];
const res = b.dataToGPU({customTexShape: texShape});
expectArraysEqual(res.texShape, texShape);

const webGLBackend = tf.backend() as MathBackendWebGL;
const buffer = webGLBackend.gpgpu.createBufferFromTexture(
    res.texture, res.texShape[0], res.texShape[1]);
const vals = webGLBackend.gpgpu.downloadFloat32MatrixFromBuffer(buffer, 36);
console.log(vals);
expectArraysEqual(vals, data);

});
The result is shown as below, it does not fill unused space with 0, and the code is related in

ivec2 resTexRC = ivec2(resultUV.yx * vec2(texShape[0], texShape[1]));

Float32Array(36) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, buffer: ArrayBuffer(144), byteLength: 144, byteOffset: 0, length: 36, Symbol(Symbol.toStringTag): 'Float32Array']

I am confusion why we need to change the shape. We could not ensure the filling content is correct and is expected by Users. What are rules to define the filling behaviors? I think it is incorrect to fill with 0. For example, if original texture is 2x2, and then call dataToGPU with shape 4x4 to create the new texture, most of the new texture is transparent, I think it is not the expected result.

@lina128
Copy link
Collaborator Author

lina128 commented Apr 22, 2022

Hi @xhcao, you only take the first 12 elements, which is correct, right? This is the same approach we use for reading data to cpu in WebGL, we create the right size of the buffer and call readPixels: https://github.com/tensorflow/tfjs/blob/master/tfjs-backend-webgl/src/gpgpu_util.ts#L271

Data beyond the size doesn't matter whether it is 0 or some other values, we simply ignore them.

@xhcao
Copy link
Contributor

xhcao commented Apr 24, 2022

@lina128 , Does it mean the data beyond the size of the new created texture must be written again before it could be rendered to canvas? There may be a wrong understanding of dataToGPU usage for me. Could you simply give an usage example of dataToGPU with custom size here. Thank you.

@lina128
Copy link
Collaborator Author

lina128 commented Apr 25, 2022

Hi @xhcao, I see what you mean. There should be another shader to format how it wants to render if the size doesn't match the canvas size. Can you describe a real use case? It will help me prepare the example. Thanks.

@xhcao
Copy link
Contributor

xhcao commented Apr 26, 2022

Hi, @lina128, actually, I did not know the scenarios which could gain the performance improvement by using custom size option, so I asked you why we needed the custom size option.
Now, I may image a scenario as below.
[1]a tensor (shape: 2x4) calling dataToGPU with custom size (4x4) to create a new bigger texture A(4x4), the first half content of A is equal to the tensor. Then we use another shader to fill the other half content of A, render the texture A to the canvas at last.

We could also use the below steps to implement the above scenario.
[2]A tensor (shape: 2x4) calling dataToGPU without custom size to create a texture A(2x4), the content of A is equal to the tensor, then we use another shader to create the texture B(2x4) and fill the content, individually render the texture A and texture B to the first half and second half of the canvas at last.

I am not sure whether [1] is better than [2], but I sort of agree to add the option to dataToGPU api if there is no side effect.

@lina128
Copy link
Collaborator Author

lina128 commented Apr 28, 2022

Hi @xhcao, thank you for providing the imagined scenarios. The actual scenario of using customSize is more like the other way round, the customSize will fit exactly the data, whereas our default (the more squarish texture) will waste some space.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants