Почему «точный» квалификатор не вступает в силу? - PullRequest
6 голосов
/ 25 мая 2019

Я пытаюсь улучшить GLSL-реализацию Генри Таслера с двойной арифметикой (из его демонстрации Мандельброта GLSL) для надежной работы с графикой NVIDIA в Linux. Недавно я узнал, что начиная с OpenGL 4.0 ( §4.7 Precise Qualifier в спецификации ) или с расширением GL_ARB_gpu_shader5 ( spec ) мы можем использовать * Спецификатор 1008 * для выполнения расчетов в соответствии с точной последовательностью арифметических операций, указанной в источнике GLSL.

Но следующая попытка, похоже, не дает никакого улучшения:

#version 330
#extension GL_ARB_gpu_shader5 : require

vec2 ds_add(vec2 dsa, vec2 dsb)
{
    precise float t1 = dsa.x + dsb.x;
    precise float e = t1 - dsa.x;
    precise float t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;

    precise vec2 dsc;
    dsc.x = t1 + t2;
    dsc.y = t2 - (dsc.x - t1);
    return dsc;
}

Результат такой же, как если бы не было добавлено precise. Я проверил, что сам алгоритм правильный: он работает как есть (даже без precise) на встроенной графике Intel Core i7-4765T, и если я скрываю некоторые переменные, чтобы запретить оптимизацию, то NVidia также дает правильные результаты , Вот как я подавляю оптимизации:

#version 330

#define hide(x) ((x)*one)
uniform float one=1;

vec2 ds_add(vec2 dsa, vec2 dsb)
{
    float t1 = dsa.x + dsb.x;
    float e = hide(t1) - dsa.x;
    float t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;

    vec2 dsc;
    dsc.x = t1 + t2;
    dsc.y = t2 - (hide(dsc.x) - t1);
    return dsc;
}

Итак, по-видимому, я неправильно использую квалификатор precise. Но что именно здесь не так?

Для справки я использую NVidia GeForce GTX 750Ti с двоичным драйвером nvidia 390.116. Вот полный тест C ++:

#include <cmath>
#include <vector>
#include <string>
#include <limits>
#include <iomanip>
#include <iostream>
// glad.h is generated by the following command:
// glad --out-path=. --generator=c --omit-khrplatform --api="gl=3.3" --profile=core --extensions=
#include "glad/glad.h"
#include <GL/freeglut.h>
#include <glm/glm.hpp>
using glm::vec4;

GLuint vao, vbo;
GLuint texFBO;
GLuint program;
GLuint fbo;
int width=1, height=2;

void printShaderOutput(int texW, int texH)
{
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texFBO);

    std::vector<vec4> data(texW*texH);
    glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, data.data());
    std::cout << "a,b,sum,relError(sum),note\n";
    for(int i=0;i<width;++i)
    {
        const auto a=double(data[i+width*0].x)+double(data[i+width*0].y);
        const auto b=double(data[i+width*0].z)+double(data[i+width*0].w);
        const auto sum=double(data[i+width*1].x)+double(data[i+width*1].y);
        const auto trueSum=a+b;
        const auto sumErr=(sum-trueSum)/trueSum;
        std::cout << std::setprecision(std::numeric_limits<double>::max_digits10)
                  << a << ',' << b << ','
                  << sum << ','
                  << std::setprecision(3)
                  << sumErr << ','
                  << (std::abs(sumErr)>1e-14 ? "WARN" : "OK")
                  << '\n';
    }
    std::cout.flush();
}

GLuint makeShader(GLenum type, std::string const& srcStr)
{
    const auto shader=glCreateShader(type);
    const GLint srcLen=srcStr.size();
    const GLchar*const src=srcStr.c_str();
    glShaderSource(shader, 1, &src, &srcLen);
    glCompileShader(shader);
    GLint status=-1;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    assert(glGetError()==GL_NO_ERROR);
    assert(status);
    return shader;
}

void loadShaders()
{
    program=glCreateProgram();

    const auto vertexShader=makeShader(GL_VERTEX_SHADER, 1+R"(
#version 330
in vec4 vertex;
void main() { gl_Position=vertex; }
)");
    glAttachShader(program, vertexShader);

    const auto fragmentShader=makeShader(GL_FRAGMENT_SHADER, 1+R"(
#version 330
#extension GL_ARB_gpu_shader5 : require

vec2 ds_add(vec2 dsa, vec2 dsb)
{
    precise float t1 = dsa.x + dsb.x;
    precise float e = t1 - dsa.x;
    precise float t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;

    precise vec2 dsc;
    dsc.x = t1 + t2;
    dsc.y = t2 - (dsc.x - t1);
    return dsc;
}

uniform vec2 a, b;
out vec4 color;

void main()
{
    if(gl_FragCoord.y<1)   // first row
        color=vec4(a,b);
    else if(gl_FragCoord.y<2)   // second row
        color=vec4(ds_add(a,b),0,0);
}

)");
    glAttachShader(program, fragmentShader);

    glLinkProgram(program);
    GLint status=0;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    assert(glGetError()==GL_NO_ERROR);
    assert(status);

    glDetachShader(program, fragmentShader);
    glDeleteShader(fragmentShader);

    glDetachShader(program, vertexShader);
    glDeleteShader(vertexShader);
}

void setupBuffers()
{
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    const GLfloat vertices[]=
    {
        -1, -1,
         1, -1,
        -1,  1,
         1,  1,
    };
    glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
    constexpr GLuint attribIndex=0;
    constexpr int coordsPerVertex=2;
    glVertexAttribPointer(attribIndex, coordsPerVertex, GL_FLOAT, false, 0, 0);
    glEnableVertexAttribArray(attribIndex);
    glBindVertexArray(0);
}

bool init()
{
    if(!gladLoadGL())
    {
        std::cerr << "Failed to initialize GLAD\n";
        return false;
    }
    if(!GLAD_GL_VERSION_3_3)
    {
        std::cerr << "OpenGL 3.3 not supported\n";
        return false;
    }

    glGenTextures(1, &texFBO);
    glGenFramebuffers(1,&fbo);

    loadShaders();
    setupBuffers();

    glViewport(0,0,width,height);

    glBindTexture(GL_TEXTURE_2D,texFBO);
    glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr);
    glBindTexture(GL_TEXTURE_2D,0);
    glBindFramebuffer(GL_FRAMEBUFFER,fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texFBO,0);
    const auto status=glCheckFramebufferStatus(GL_FRAMEBUFFER);
    assert(status==GL_FRAMEBUFFER_COMPLETE);
    glBindFramebuffer(GL_FRAMEBUFFER,0);

    return true;
}

void display()
{
    const static bool inited=init();
    if(!inited) std::exit(1);

    glBindFramebuffer(GL_FRAMEBUFFER,fbo);

    glUseProgram(program);
#define SPLIT_DOUBLE_TO_FLOATS(x) GLfloat(x),GLfloat(x-GLfloat(x))
    glUniform2f(glGetUniformLocation(program,"a"),SPLIT_DOUBLE_TO_FLOATS(3.1415926535897932));
    glUniform2f(glGetUniformLocation(program,"b"),SPLIT_DOUBLE_TO_FLOATS(2.7182818284590452));
    glUniform1f(glGetUniformLocation(program,"rtWidth"),width);

    glBindVertexArray(vao);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glBindVertexArray(0);

    printShaderOutput(width, height);
    std::exit(0);

    glFinish();
}

int main(int argc, char** argv)
{
    glutInitContextVersion(3,3);
    glutInitContextProfile(GLUT_CORE_PROFILE);
    glutInit(&argc, argv);

    glutInitDisplayMode(GLUT_RGB);

    glutInitWindowSize(width, height);
    glutCreateWindow("Test");
    glutDisplayFunc(display);

    glutMainLoop();
}

Мне удалось извлечь сборку NVfp5.0 из двоичных файлов программы GLSL в различных случаях:

  • Наивный кейс без hide и без precise:
!!NVfp5.0
OPTION NV_internal;
OPTION NV_bindless_texture;
PARAM c[2] = { program.local[0..1] };
TEMP R0;
TEMP T;
TEMP RC, HC;
OUTPUT result_color0 = result.color;
SLT.F R0.x, fragment.position.y, {1, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
MOV.F result_color0.xy, c[0];
MOV.F result_color0.zw, c[1].xyxy;
ELSE;
SLT.F R0.x, fragment.position.y, {2, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
ADD.F R0.y, -c[0].x, c[0].x;
ADD.F R0.x, -c[1], c[1];
ADD.F R0.x, R0, R0.y;
ADD.F R0.x, R0, c[0].y;
ADD.F R0.y, R0.x, c[1];
ADD.F R0.x, c[0], c[1];
ADD.F result_color0.x, R0, R0.y;
ADD.F result_color0.y, R0, -R0;
MOV.F result_color0.zw, {0, 0, 0, 0}.x;
ENDIF;
ENDIF;
END
  • Случай с precise (обратите внимание, что ничего не изменится, кроме суффикса .PREC в «инструкциях»):
!!NVfp5.0
OPTION NV_internal;
OPTION NV_bindless_texture;
PARAM c[2] = { program.local[0..1] };
TEMP R0;
TEMP T;
TEMP RC, HC;
OUTPUT result_color0 = result.color;
SLT.F R0.x, fragment.position.y, {1, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
MOV.F result_color0.xy, c[0];
MOV.F result_color0.zw, c[1].xyxy;
ELSE;
SLT.F R0.x, fragment.position.y, {2, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
ADD.F.PREC R0.y, -c[0].x, c[0].x;
ADD.F.PREC R0.x, -c[1], c[1];
ADD.F.PREC R0.x, R0, R0.y;
ADD.F.PREC R0.x, R0, c[0].y;
ADD.F.PREC R0.y, R0.x, c[1];
ADD.F.PREC R0.x, c[0], c[1];
ADD.F.PREC result_color0.x, R0, R0.y;
ADD.F.PREC result_color0.y, R0, -R0;
MOV.F result_color0.zw, {0, 0, 0, 0}.x;
ENDIF;
ENDIF;
END
  • Случай с hide, который работает и, очевидно, имеет другую последовательность арифметических операций:
!!NVfp5.0
OPTION NV_internal;
OPTION NV_bindless_texture;
PARAM c[3] = { program.local[0..2] };
TEMP R0, R1;
TEMP T;
TEMP RC, HC;
OUTPUT result_color0 = result.color;
SLT.F R0.x, fragment.position.y, {1, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
MOV.F result_color0.xy, c[1];
MOV.F result_color0.zw, c[2].xyxy;
ELSE;
SLT.F R0.x, fragment.position.y, {2, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
ADD.F R0.x, c[1], c[2];
MAD.F R0.y, R0.x, c[0].x, -c[1].x;
ADD.F R0.z, R0.x, -R0.y;
ADD.F R0.z, -R0, c[1].x;
ADD.F R0.y, -R0, c[2].x;
ADD.F R0.y, R0, R0.z;
ADD.F R0.y, R0, c[1];
ADD.F R0.y, R0, c[2];
ADD.F R1.x, R0, R0.y;
MAD.F R0.x, R1, c[0], -R0;
MOV.F R1.zw, {0, 0, 0, 0}.x;
ADD.F R1.y, R0, -R0.x;
MOV.F result_color0, R1;
ENDIF;
ENDIF;
END

Ответы [ 2 ]

1 голос
/ 04 июня 2019

Я никогда не использовал точный сам, хотя вы можете извлечь пользу из изучения OpenCL или CUDA здесь.

В любом случае, ваша GLSL-версия - 3.30, которая связана с OpenGL 3.3 ,Точный квалификатор доступен через расширение, но я всегда буду пытаться использовать встроенную функцию OpenGL, если вы можете.

Расширение не может быть реализовано таким же образом, я предлагаю вам попробовать напо крайней мере, GLSL версии 4.0, в идеале - последняя версия OpenGL / GLSL.

Иногда эти старые расширения могут иметь регрессию на новых графических процессорах, если их никто не использует.

Компиляторы графических процессоров, как правило, более либеральны соптимизаций.Вы можете получить выгоду от просмотра выходных данных от скомпилированного шейдера, может быть какой-то способ просмотреть выходные данные сборки PTX от компилятора Nvidia с GLSL.С CUDA вы можете предварительно просмотреть выходные данные сборки, чтобы убедиться, что компилятор не переупорядочивает операции.

Спецификация указывает MAD как главную причину для классификатора - это заставит компилятор неиспользовать инструкцию MAD.Возможно, было проведено небольшое тестирование с сложением / вычитанием с точным классификатором.

Если hide решит это за вас, вероятно, лучше просто назвать это днем, я сомневаюсь, что точный классификатор был тщательно проверен на стороне GLSL,Я настоятельно рекомендую CUDA или OpenCL для этого, вы можете использовать CL-GL interop, если вы хотите также быстро отобразить текстуру, что не очень болезненно.

Точный квалификатор гарантирует отсутствие переупорядочения операций, но не упоминает оптимизации, которые не влияют на упорядочение.Похоже, AMD просто отключает оптимизацию при его использовании.Все еще возможно, что Nvidia по-прежнему применяет оптимизации, влияющие на ваш результат, которые связаны не с порядком операций, а с конкретными оптимизациями для выполняемого добавления.

precise float t1 = dsa.x + dsb.x;
precise float e = t1 - dsa.x;

Это, вероятно, будет вычислять e какпросто dsb.x.Компилятор потенциально может все еще добавлять оптимизации, которые не влияют на порядок операций, поскольку это все, что гарантирует спецификация.Я не могу думать ни о чем, кроме переупорядочиваемых операций, которые могут повлиять на этот результат, но я здесь не эксперт.

Еще одна вещь, на которую следует обратить внимание, это то, что, исходя из моего беглого прочтения спецификации,результат из ds_add может потребоваться сохранить в точную переменную, чтобы вычисления были точными.Функция может быть встроена только в Nvidia (у них гораздо лучшая оптимизация, по крайней мере, исторически), поэтому я представляю, что компилятор может выполнить встраивание, а затем, если вы сохраните результат в неточной переменной, то все существующие точные классификаторы будутпроигнорирован.

0 голосов
/ 29 мая 2019

Ничего плохого в вашем шейдере. В коде ds_add () просто нет операций, которые можно было бы объединить во время компиляции. Обычно добавляют и умножают / делят слияние. Но в вашем коде есть только операции добавления.

Обновление:

  1. В случае, если все ваши переменные сохраняются в регистрах GPU в процессе расчета. Порядок операций с регистрами не зависит от кода или компилятора. Это даже не зависит только от оборудования. Это зависит от текущих выполняемых операций в графическом процессоре.

  2. Точность операций с плавающей запятой между регистрами не является строго 32-битной. Это обычно выше тогда. Фактическая точность для графических процессоров является коммерческой тайной. Фактическая точность для x86 FPU составляет 80 или 128 бит, несмотря на то, что переменные хранятся в 32-битной памяти.

  3. Однако графические процессоры не предназначены для очень точного расчета. Автор алгоритма знает это и реализует пары двойных мыслей 32-битных операций с плавающей запятой. Если вам нужно повысить точность, то вы должны использовать long double с четырьмя 32-битными числами с плавающей точкой. Простое «точное» не помогает.

...