I suggest start from a simpler BRDF to make sure that your main loop is not broken -- try something simple like lambert: max(0,dot(lightRay,hitNormal))
and be sure that those are normalized vectors. Divide by scene->lights.size()
if it's simply too bright because you have too many lights.
If the image looks correct with a simple BRDF, now just try it with variations of your other components. You don't give the code for lookup_brdf_val()
at all, so beyond that one can only speculate.
It's just like any other programming, though. Reduce the # of variables until you find the one that's awry.