If you can modify the builtin_max then the way to go is changing the signature so that it takes two constant references and returns a constant reference (unless the type is small enough that pass-by-value makes sense), and overload for the non-const version of it. Then you can adapt the signatures the easy way: just forward the call.
fract const & builtin_max( fract const & lhs, fract const & rhs );
fract & builtin_max( fract & lhs, fract & rhs );
template <>
fract const & std::max( fract const & lhs, fract const & rhs ) {
   return builtin_max( lhs, rhs );
}
template <>
fract & std::max( fract & lhs, fract & rhs ) {
   return builtin_max( lhs, rhs );
}
Another simple thing that you can do is not overload std::max but rather produce your own max function within your namespace. All unqualified uses of max that take fract values will find your max function before they try to use the std::max default template. Then again, this will not work for fully qualified calls to std::max:
namespace x {
   class fract;
   fract max( fract lhs, fract rhs ) { return builtin_max( lhs, rhs ); }
}
// force a link time error if fully qualified std::max is used (undefined)
// instead of silently getting std::max to use < (if it is defined)
// if there is no operator<( fract const &, fract const & ), leave this out
// to get an earlier compile time error
template <> fract const & std::max( fract const &, fract const & );
template <> fract & std::max( fract &, fract & );
int main() {
   using namespace std;
   fract a,b;
   max( a, b );          // x::max
   // std::max( a, b )   // ouch, link time error
}