-
Notifications
You must be signed in to change notification settings - Fork 12.1k
finetune.cpp command-line arg #13873
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
base: master
Are you sure you want to change the base?
Conversation
perhaps no need to review until i have an actual SGD impl in a follow-on, @JohannesGaessler - but a few general questions about contributing:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should better keep that change as it time to get more feedbacks/approval.
Any changes made to the ggml source in this repository will eventually be synced to the ggml repository and vice versa; it is completely fine. I think the issue of a git submodule was previously brought up and rejected.
My opinion is that people serious about training should be writing a program rather than use a command line tool. Still, I think it's good to make things such as the learning rate configurable in the provided example program.
I don't remember whether those args were put in by me when I copypasted code or by Georgi when he later refactored it but I myself definitely did not make an intentional choice to use these exact arguments.
I don't know, sorry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the previous perplexity-specific arguments are needed.
For adding an SDG optimizer, add a new ggml op like |
yes, will do. should the actual SGD impl be a subsequent pull req (or several, e.g. starting first w/ just CPU impl) or do you want it all in one pull req? |
Either way would be fine with me as long as there are at no point broken or unfinished features on master. |
e752031
to
e689af8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking forward to the next PR(s).
you should see frivolous clang-format changes (using the project's .clang-format) only on lines changed in the PR (using git-clang-format). if there's something undesireable we could figure out what in the format config does it |
Don't autoformat code en masse unless it's done in a dedicated PR, it makes it unnecessarily difficult to track what was actually changed in a PR. |
Sorry, I didn't read the
part. |
7534bbf
to
48a16bf
Compare
Hi @WilliamTambellini @JohannesGaessler I think this is usable now, inviting code nitpicks etc :) |
Second (actual usable SGD) commit is 48a16bf (also shows above here) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mix up different projects: change of CLI/renaming and SGD. Need to split in 2 PRs.
@slaren ?
ggml/src/ggml-opt.cpp
Outdated
@@ -770,7 +814,7 @@ void ggml_opt_eval(ggml_opt_context_t opt_ctx, ggml_opt_result_t result) { | |||
// beta1, beta2 after applying warmup | |||
const float beta1h = 1.0f/(1.0f - powf(opt_pars.adamw.beta1, opt_ctx->iter)); | |||
const float beta2h = 1.0f/(1.0f - powf(opt_pars.adamw.beta2, opt_ctx->iter)); | |||
|
|||
const float keep = 1.0f - opt_pars.adamw.alpha * opt_pars.adamw.wd; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optimizer steps are going to be I/O bound and optimizing compute is not going to make a meaningful difference for the runtime of the steps, for the runtime of the total probram it's completely negligible. So please revert this change, I think the other variant is easier to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that it's not likely to matter, but it's 1. per parameter per epoch (ok, does seem unimportant now that I think further) and 2. i'm not confident cuda CC optimizes this and was hoping to learn more - would seem possible that w/o this we're loading repeatedly two floats instead of one - and mostly 3. this is exactly following precedent established for beta1h and beta2h, which are stored in the tensor just as i stored this quantity.
Anyway, totally willing, just curious what you think about the existing practice of saving beta1h and beta2h in light of this opinion that we're not compute bound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i checked it out - doesn't seem to change runtime noticeably as you predicted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My biggest concern with the code is the amount of effort needed to maintain it, particularly when it comes to debugging and asserting that the code on master works correctly. It is quite likely that I will at some point be in a situation where a user reports bad training results and I will not know whether that is the due to a bug in ggml or due to bad hyperparamters or something similar. So it is very important to me that the data layout is consistent across multiple levels.
The correct way to implement the micro-optimization of pre-computing a parameter derived from the human-interpretable parameters is as follows:
- Pass the human-interpretable parameters to
ggml_opt_step_adamw
/ggml_opt_step_sdg
. - In the CUDA host code, pre-compute some derived parameters from the human-interpretable parameters.
- Change the CUDA device code to accept the derived parameters instead.
The way CUDA works is that the CPU schedules the GPU kernels in a CUDA stream and then waits for said stream to finish all kernels. Scheduling the kernels is of course much faster and it doesn't matter how fast you are as long as you are fast enough to keep the GPU busy. So adding a bit of overhead to the scheduling has essentially no impact on the runtime of a CUDA program even if you do it once per CUDA kernel launch instead of once per epoch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaining all that, the bottom line for me is that you were right and the micro-optimization has no visible benefit in this case.
9fbe596
to
7b9a0f2
Compare
I want it removed as a CLI argument, the parameterization should be done via |
This comment was marked as resolved.
This comment was marked as resolved.
confirmed: llama_context: n_ctx = 2688 fits in memory exceeding the larget possible w/ opt_period 1: (couldn't do double, because of the extra accumulation memory needed w/ period>1) |
3a41cf4
to
e30abe6
Compare
i think we're caught up - double checked test-opt to make sure it still passes, do you want it re-disabled? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prior to my review there were already unresolved requests for changes that will need to be addressed prior to merging.
common/arg.cpp
Outdated
{ "-lr-half", "--learning-rate-halflife-epochs" }, "N", | ||
string_format("reduce lr in half every N epochs (default: %.3g)", (double) params.lr.halflife_epochs), | ||
[](common_params & params, const std::string & value) { params.lr.halflife_epochs = std::stof(value); }) | ||
.set_examples({ LLAMA_EXAMPLE_FINETUNE })); | ||
add_opt(common_arg({ "-lr-halvings", "--learning-rate-halvings" }, "N", | ||
string_format("max N lr halvings (default: %.3g)", (double) params.lr.halvings), | ||
[](common_params & params, const std::string & value) { params.lr.halvings = std::stof(value); }) | ||
.set_examples({ LLAMA_EXAMPLE_FINETUNE })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current code the final learning rate you get is relative to the initial learning rate. That is what I am taking issue with because to me it seems like it would make more sense to adjust the two values independently from one another. Please change the argument to something like -lr-min
.
tests/test-opt.cpp
Outdated
return result; | ||
} | ||
|
||
static enum ggml_opt_optimizer_type g_optimizer_type = GGML_OPT_OPTIMIZER_TYPE_ADAMW; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't introduce global state to the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're asking me to pass this around as a parameter everywhere instead, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
I looked at the git history. When I overhauled the ggml training code I re-used a pre-existing path |
if you prefer lr-min enough to do it yourself, great. otherwise i'll decline |
other currently outstanding questions are (let me know if i missed any): no globals allowed in unit test (!), and 'set these values' (lr_opt) |
If we can't reach an agreement regarding the exact implementation regarding a decaying learning rate I would say to just strip the feature from this PR. The other pre-existing, unresolved issues I mentioned were the ones in Sorry for being difficult, social skills are not my strong point. |
same :) |
fcc241e
to
856d37c
Compare
any update? all handled on my end afaict |
As I already said, please look at my comments in I am not willing to maintain an interface that has two parameterizations for the same thing. I am willing to maintain the I started the CI. Once the above two issues are addressed and the CI works I will push some purely cosmetic changes and then I would be willing to merge this PR. |
Also there is still global state in the unit tests. |
i'll fix, thanks for the reminder |
add unit tested GGML_OPT_OPTIMIZER_SGD to ggml - avoids allocating m, v tensors. support finetune.cpp arg -opt SGD (or sgd). (default adamw as before) llama 3.2-1b-F32 result: observed 11gb gpu ram (41 sec/epoch) when using SGD instead of 19gb (55 sec/epoch) using adamw. (wikipedia 100 lines finetune) ( using the same GPU memory, adamw can only do before OOM 512 batch/context, reaching: train: [███████▉] data=0000140/0000140 loss=0.02575±0.00099 acc=99.52±0.03% t=00:00:47 ETA=00:00:00 val: [███████▉] data=0000008/0000008 loss=4.76565±0.28810 acc=41.46±0.77% t=00:00:00 ETA=00:00:00 SGD is superior, though it converges slower, with max before OOM 1728 batch/context (esp see the better validation perf): train: [███████▉] data=0000039/0000039 loss=0.00371±0.00010 acc=99.96±0.01% t=00:00:41 ETA=00:00:00 val: [███████▉] data=0000003/0000003 loss=5.11406±0.76034 acc=48.01±0.69% t=00:00:01 ETA=00:00:00 ) note: when finetuning long enough (or w/ enough -lr), validation accuracy *eventually* drops ('catastrophic forgetting') -lr-half (halflife) option useful for SGD to avoid oscillation or super slow underdamped learning (makes setting -lr more forgiving). terminal -lr for now is set by lr-halvings i.e. if you want at most 1/8 the inital -lr you set -lr-halvings 3. note: objective loss not directly comparable between adamw, sgd? - check perplexity or accuracy or consider relative improvements for convergence new finetune args -wd 1e-9 to enable weight decay in sgd or adamw, and max -epochs N (default 2 as before) cache (1 - wd*alpha) in 'adamw' opt struct - no noticeable perf benefit, disabled (still done for new SGD though) since opt. memory is pre-allocated, the ggml_opt_get_optimizer_params would probably be able to change between SGD and AdamW with each epoch but would need to use adamw for the first (unconfirmed - no cmdline arg to set such a policy yet) test-opt checks adamw as before and now sgd (except for a few disabled tests for sgd only; probably just needs logging values and adding alternate reference values); tolerance on the 'regression' test is broader for sgd (so we don't need many more epochs)
pushed: removed 'half' related args, verify lr0/lr-min behavior, removed global var from test-opt (verified still passes) please let me know if you find any other change you want as i'm keen to wrap this up |
and no objection to any cosmetics you want to slap on yourself, of course! |
add to ggml-opt learning rate (adamw alpha) cmdline arg, and an optimizer enum defaulting to adamw,
preparatory to work to support SGD
these are in common args a set of optimizer options active only for the new FINETUNE example (which includes all the previous finetune.cpp PERPLEXITY options as a precaution)
perhaps breaking with precedent, the ggml_opt_optimizer_params struct is included directly as args - if desired, we can instead just add learning rate and optimizer type to a struct independent of ggml-opt.h
as proposed in
#13835