From 865fd4b5cd3f237bab0d48afa23be2839af20768 Mon Sep 17 00:00:00 2001 From: 20kdc Date: Sun, 30 May 2021 18:42:35 +0100 Subject: [PATCH] VGG7 example --- examples/neural_network_vgg7/.gitignore | 1 + examples/neural_network_vgg7/README.md | 12 ++ examples/neural_network_vgg7/import_vgg7.py | 30 +++++ examples/neural_network_vgg7/run_vgg7.py | 125 ++++++++++++++++++++ examples/neural_network_vgg7/sh_common.py | 82 +++++++++++++ examples/neural_network_vgg7/sh_conv.py | 70 +++++++++++ examples/neural_network_vgg7/w2wbinit.png | Bin 0 -> 14106 bytes 7 files changed, 320 insertions(+) create mode 100644 examples/neural_network_vgg7/.gitignore create mode 100644 examples/neural_network_vgg7/README.md create mode 100644 examples/neural_network_vgg7/import_vgg7.py create mode 100644 examples/neural_network_vgg7/run_vgg7.py create mode 100644 examples/neural_network_vgg7/sh_common.py create mode 100644 examples/neural_network_vgg7/sh_conv.py create mode 100644 examples/neural_network_vgg7/w2wbinit.png diff --git a/examples/neural_network_vgg7/.gitignore b/examples/neural_network_vgg7/.gitignore new file mode 100644 index 000000000..8a402f0be --- /dev/null +++ b/examples/neural_network_vgg7/.gitignore @@ -0,0 +1 @@ +model-kipper diff --git a/examples/neural_network_vgg7/README.md b/examples/neural_network_vgg7/README.md new file mode 100644 index 000000000..187da42e3 --- /dev/null +++ b/examples/neural_network_vgg7/README.md @@ -0,0 +1,12 @@ +# Waifu2x VGG7 implementation + +This demonstrates performing image upscaling using Python and vulkan-kompute. + +To import an existing VGG7 model (assuming you have https://github.com/nagadomi/waifu2x/ cloned somewhere): + +`python3 import_vgg7.py waifu2x/models/vgg_7/art/scale2.0x_model.json` + +To execute that model (no tiling is performed, so be careful about image sizes): + +`python3 run_vgg7.py w2wbinit.png out.png` + diff --git a/examples/neural_network_vgg7/import_vgg7.py b/examples/neural_network_vgg7/import_vgg7.py new file mode 100644 index 000000000..c86ff36c8 --- /dev/null +++ b/examples/neural_network_vgg7/import_vgg7.py @@ -0,0 +1,30 @@ +import numpy +import json +import os +import sys +import time +import sh_common + +if len(sys.argv) != 2: + print("import_vgg7.py JSONPATH") + print(" i.e. import_vgg7.py /home/you/Documents/External/waifu2x/models/vgg_7/art/scale2.0x_model.json") + sys.exit(1) + +try: + os.mkdir("model-kipper") +except: + pass + +data_list = json.load(open(sys.argv[1], "rb")) + +idx = 0 +for i in range(7): + layer = data_list[i] + w = numpy.array(layer["weight"]) + w.reshape((-1, 3, 3)).transpose((0, 2, 1)) + b = numpy.array(layer["bias"]) + sh_common.save_param("kipper", idx, w) + idx += 1 + sh_common.save_param("kipper", idx, b) + idx += 1 + diff --git a/examples/neural_network_vgg7/run_vgg7.py b/examples/neural_network_vgg7/run_vgg7.py new file mode 100644 index 000000000..f5d88e841 --- /dev/null +++ b/examples/neural_network_vgg7/run_vgg7.py @@ -0,0 +1,125 @@ +import kp +import numpy +import os +import sys +import time +import sh_conv +import sh_common + +if len(sys.argv) != 3: + print("run_vgg7.py INPUT OUTPUT") + print(" Tiling is not implemented, but padding is implemented") + sys.exit(1) + +# NOTES: +# + Tiling is not implemented, but padding is implemented +# So don't run anything too big through it + +if False: + kpm = kp.Manager(1) + if kpm.get_device_properties()["device_name"].count("RAVEN") > 0: + raise "Safety cut-out triggered. Sorry!" +else: + kpm = kp.Manager() + +image = sh_common.image_load(sys.argv[1]) +image = image.repeat(2, 0).repeat(2, 1) +image = numpy.pad(image, [[7, 7], [7, 7], [0, 0]], mode = "edge") + +# Ensure image has 4 channels even though they will be unused. +# This is because of vectorization vec4 magic. +while image.shape[2] < sh_common.VSZ: + image = numpy.pad(image, [[0, 0], [0, 0], [0, 1]], mode = "constant") + +# sh_common.image_save("pad.png", image) + +# Prepare the initial tensor. + +tensor_in = kpm.tensor(image) +tensor_in_h = image.shape[0] +tensor_in_w = image.shape[1] +tensor_in_cg = 1 +tensor_in_c = 3 + +# Run things. +channels = [32, 32, 64, 64, 128, 128, 3] + +for i in range(7): + # Prepare tensors. + # 'c' is the total amount of channels, while 'cg' is the amount of vec4s (channel-groups). + # This is important because weights have to be padded for the shader. + tensor_out_h = tensor_in_h - 2 + tensor_out_w = tensor_in_w - 2 + tensor_out_c = channels[i] + tensor_out_cg = (channels[i] + (sh_common.VSZ - 1)) // sh_common.VSZ + # TODO: How to produce a blank tensor we don't care about the contents of? + # This isn't being synced, and from experience so far that should handle most of it, + # but what about memory usage? + # *Most* of these tensors live entirely on-device except when debugging. + # Can that be handled? (Also good question: Does it even need to be handled?) + tensor_out = kpm.tensor(numpy.zeros((tensor_out_h * tensor_out_w * tensor_out_cg * sh_common.VSZ))) + weight = kpm.tensor(sh_common.load_weights_padded("kipper", (i * 2) + 0, tensor_out_c, tensor_in_c, 3)) + bias = kpm.tensor(sh_common.load_biases_padded("kipper", (i * 2) + 1, tensor_out_c)) + # Compute. + # TODO: It'd be nice to wrap this up into a class for optimization purposes. + workgroup = ((tensor_out_w + 7) // 8, (tensor_out_h + 1) // 2, tensor_out_cg) + alg = kpm.algorithm( + # tensors + [tensor_in, bias, weight, tensor_out], + # spirv + sh_conv.conv_shader, + # workgroup + workgroup, + # spec_consts + [tensor_in_w, tensor_in_h, tensor_in_cg, tensor_out_w, tensor_out_h, tensor_out_cg], + # push_consts + [] + ) + + print("Step complexity " + str(workgroup)) + print("Step channel layout " + str(tensor_in_cg) + " " + str(tensor_out_cg)) + + # Do this first. Keep in mind "syncs" are copies. + last_seq = kpm.sequence() + things_to_sync_to_device = [bias, weight] + if i == 0: + # For first layer, the input isn't on-device yet + things_to_sync_to_device.append(tensor_in) + last_seq.eval_async(kp.OpTensorSyncDevice(things_to_sync_to_device)) + last_seq.eval_await() + + # Prepare + seq = (kpm.sequence() + .record(kp.OpAlgoDispatch(alg, [])) + ) + # Run + seq.eval() + + print("Done with step") + + if False: + # DEBUG: + # We want to see the output, copy it to local + last_seq = kpm.sequence() + last_seq.eval_async(kp.OpTensorSyncLocal([tensor_out])) + last_seq.eval_await() + tensor_out.data().astype(" numpy.ndarray: + """ + Loads an image. + Doesn't Tensor it, in case you need to do further work with it. + Shape is (h, w, 3). + """ + # file + na = numpy.array(Image.open(path)) + # change type + na = na.astype("float32") / 255.0 + return na + +def image_save(path, na: numpy.ndarray): + """ + Saves an image. + However, note this expects a numpy array. + Shape is (h, w, 3). + """ + # change type + na = numpy.fmax(numpy.fmin(na * 255.0, 255), 0).astype("uint8") + # file + Image.fromarray(na).save(path) + +def load_param(mdl, idx, expected): + npa = numpy.fromfile("model-" + mdl + "/snoop_bin_" + str(idx) + ".bin", " + # [outputCGroups][kernelH][kernelW][inputCGroups][outputChannels][inputChannels] + weight_na = load_param(mdl, idx, tensor_out_c * tensor_in_c * weight_s * weight_s) + # start by putting in the initial shape + weight_na = weight_na.reshape(tensor_out_c, tensor_in_c, weight_s, weight_s) + # then by padding + # NOTE: It is *critically important* that weight padding is done with the "zero" mode. + # The shader WILL NOT ignore these values, but zeroing them causes them to have no effect. + if (tensor_in_c & 3) != 0: + weight_na = numpy.pad(weight_na, [[0, 0], [0, 4 - (tensor_in_c & 3)], [0, 0], [0, 0]], mode = "constant") + if (tensor_out_c & 3) != 0: + weight_na = numpy.pad(weight_na, [[0, 4 - (tensor_out_c & 3)], [0, 0], [0, 0], [0, 0]], mode = "constant") + # reshape to finish splitting things up + weight_na = weight_na.reshape(tensor_out_cg, 4, tensor_in_cg, 4, weight_s, weight_s) + # result is: + # [outputCGroups][outputChannels][inputCGroups][inputChannels][kernelH][kernelW] + # and move output channels to the right... + weight_na = numpy.moveaxis(weight_na, 1, 5) + # result is: + # [outputCGroups][inputCGroups][inputChannels][kernelH][kernelW][outputChannels] + # and move input channels to the right... + weight_na = numpy.moveaxis(weight_na, 2, 5) + # result is: + # [outputCGroups][inputCGroups][kernelH][kernelW][outputChannels][inputChannels] + # and move input cgroups to the right... + weight_na = numpy.moveaxis(weight_na, 1, 3) + return weight_na + +def load_biases_padded(mdl, idx, tensor_out_c): + tensor_out_cg = (tensor_out_c + 3) // 4 + # [outputCGroups][outputChannels] + # biases merely need padding + # Again, has to be zero + bias_na = load_param(mdl, idx, tensor_out_c) + if (tensor_out_c & 3) != 0: + bias_na = numpy.pad(bias_na, [[0, 4 - (tensor_out_c & 3)]], mode = "constant") + return bias_na + diff --git a/examples/neural_network_vgg7/sh_conv.py b/examples/neural_network_vgg7/sh_conv.py new file mode 100644 index 000000000..dea3722cf --- /dev/null +++ b/examples/neural_network_vgg7/sh_conv.py @@ -0,0 +1,70 @@ +import kp + +# This is the convolution & leakyrelu shader. +global conv_shader +conv_shader = kp.Shader.compile_source(""" +#version 450 + +layout (local_size_x = 8, local_size_y = 2) in; + +// [y][x][group] (vec4: channels) +layout (set = 0, binding = 0) buffer buf_in_image { readonly restrict vec4 in_image[]; }; +// [outputCGroups] (vec4: output channels) +layout (set = 0, binding = 1) buffer buf_in_bias { readonly restrict vec4 in_bias[]; }; +// [outputCGroups][kernelH][kernelW][inputCGroups] (mat4: input & output channels) +layout (set = 0, binding = 2) buffer buf_in_weight { readonly restrict mat4 in_weight[]; }; +// [y][x][group] (vec4: channels) +layout (set = 0, binding = 3) buffer buf_out_image { writeonly restrict vec4 out_image[]; }; + +// The 'c' measures in cgroups. +// Some maths changes as a result. +layout (constant_id = 0) const float in_w = 0; +layout (constant_id = 1) const float in_h = 0; +layout (constant_id = 2) const float in_cg = 0; +layout (constant_id = 3) const float out_w = 0; +layout (constant_id = 4) const float out_h = 0; +layout (constant_id = 5) const float out_cg = 0; + +uint index_in_no_ic(uvec2 pos) { + return (pos.x + (pos.y * uint(in_w))) * uint(in_cg); +} + +uint index_out(uvec2 pos) { + return ((pos.x + (pos.y * uint(out_w))) * uint(out_cg)) + gl_GlobalInvocationID.z; +} + +void main() { + // out x/y is gl_GlobalInvocationID.xy + // we need to account for workgroupy padding *here* + // so long as we aren't trying to output to a pixel that doesn't exist, + // we won't read from any pixels that don't exist + if ( + (gl_GlobalInvocationID.x < (uint(in_w) - 2)) && + (gl_GlobalInvocationID.y < (uint(in_h) - 2)) + ) { + vec4 value = in_bias[gl_GlobalInvocationID.z]; + for (uint x = 0; x < 3; x++) { + for (uint y = 0; y < 3; y++) { + uint weight_ptr = ((gl_GlobalInvocationID.z * 9) + (x + (y * 3))) * uint(in_cg); + // specific pixel + // important to note is that since in position has a border around it, + // no further transformation is necessary (the - is implied) + uvec2 in_pos = gl_GlobalInvocationID.xy + uvec2(x, y); + uint in_ptr = index_in_no_ic(in_pos); + for (uint icg = 0; icg < uint(in_cg); icg++) { + // input channel group + vec4 iCG = in_image[in_ptr]; + // handle all 4 input components + value += iCG * in_weight[weight_ptr]; + weight_ptr += 1; + in_ptr += 1; + } + } + } + // leakyrelu slope 0.1 + value = (max(value, 0.0) * 0.9) + (value * 0.1); + out_image[index_out(gl_GlobalInvocationID.xy)] = value; + } +} +""") + diff --git a/examples/neural_network_vgg7/w2wbinit.png b/examples/neural_network_vgg7/w2wbinit.png new file mode 100644 index 0000000000000000000000000000000000000000..fc3a908e52a8103132f33e9109d4969938b1c4f4 GIT binary patch literal 14106 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 G@Bast{e`0d literal 0 HcmV?d00001